From fc6877be9fdb3cebb654058904af2af205620760 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sat, 25 Jun 2016 13:53:54 +0200 Subject: [PATCH 1/9] Fix up install_requires for Python3 --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8afdd59..adae6d7 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,14 @@ from setuptools import setup tests_require = [ - 'nose', 'exam', 'mock', 'nose-performance', 'django', 'redis', 'unittest2' + 'nose', 'exam', 'mock', 'django', 'redis' ] +if sys.version_info < (3, 3): + tests_require.append('unittest2') + tests_require.append('nose-performance') + + setup_requires = [] if 'nosetests' in sys.argv[1:]: setup_requires.append('nose') @@ -39,6 +44,7 @@ 'durabledict>=0.9.0', 'jsonpickle', 'werkzeug', + 'six', ], setup_requires=setup_requires, namespace_packages=['gutter'], From 49043bb8c0547a5410b14ec8ab260802bbcebfa3 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sat, 25 Jun 2016 13:54:18 +0200 Subject: [PATCH 2/9] Add nix environment for Python3 development. --- default.nix | 61 ++++++++++++++++++++++++ python-packages.nix | 112 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 default.nix create mode 100644 python-packages.nix diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..4b78e9f --- /dev/null +++ b/default.nix @@ -0,0 +1,61 @@ +{ pkgs ? (import {}), pythonPackages ? "python35Packages" }: +let + basePythonPackages = with builtins; if isAttrs pythonPackages + then pythonPackages + else getAttr pythonPackages pkgs; + + inherit (pkgs.lib) fix extends; + elem = builtins.elem; + basename = path: with pkgs.lib; last (splitString "/" path); + startsWith = prefix: full: let + actualPrefix = builtins.substring 0 (builtins.stringLength prefix) full; + in actualPrefix == prefix; + + src-filter = path: type: with pkgs.lib; + let + ext = last (splitString "." path); + in + !elem (basename path) [".git" "__pycache__" ".eggs"] && + !elem ext ["egg-info" "pyc"] && + !startsWith "result" path; + + gutter-src = builtins.filterSource src-filter ./.; + + localOverrides = self: super: { + gutter = super.gutter.override (attrs: { + src = gutter-src; + }); + durabledict = super.durabledict.override (attrs: { + # Needs correct import from durabledict.redis, >0.9.2 + src = pkgs.fetchurl { + url = https://github.com/disqus/durabledict/archive/17dfccdcee18da4df7c9dbc0557dc27a4ea6f3cc.zip; + sha256 = "16xmsa57lwzd53s80id73722c0bj6378lbbfpzd6kks0rirkfpcv"; + }; + }); + traceback2 = super.traceback2.override (attrs: { + buildInputs = attrs.buildInputs ++ ( + with super; [pbr]); + }); + + linecache2 = super.linecache2.override (attrs: { + buildInputs = attrs.buildInputs ++ ( + with super; [pbr]); + }); + }; + + pythonPackagesWithLocals = self: basePythonPackages.override (a: { + inherit self; + }) + // (scopedImport { + self = self; + super = basePythonPackages; + inherit pkgs; + inherit (pkgs) fetchurl fetchgit; + } ./python-packages.nix); + + myPythonPackages = + (fix + (extends localOverrides + pythonPackagesWithLocals)); + +in myPythonPackages.gutter diff --git a/python-packages.nix b/python-packages.nix new file mode 100644 index 0000000..fb3c3e1 --- /dev/null +++ b/python-packages.nix @@ -0,0 +1,112 @@ +{ + durabledict = super.buildPythonPackage { + name = "durabledict-0.9.2"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/b2/b4/055cafa805ff3872c51371825e88119ac40d3ff09597505ffe407ced33a7/durabledict-0.9.2.tar.gz"; + md5 = "e656bd013fcd41cf74566b1ccf672adc"; + }; + }; + gutter = super.buildPythonPackage { + name = "gutter-0.6.0"; + buildInputs = with self; [nose exam mock django redis]; + doCheck = true; + propagatedBuildInputs = with self; [durabledict jsonpickle werkzeug six]; + src = ./.; + }; + jsonpickle = super.buildPythonPackage { + name = "jsonpickle-0.9.3"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/2a/9f/bc2833f0c0dbe2bcca7684765ca93ec34344704b9d27a3a2e0f362bad8e9/jsonpickle-0.9.3.tar.gz"; + md5 = "cb30198969da11f9d19a11d03e0d0046"; + }; + }; + six = super.buildPythonPackage { + name = "six-1.10.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz"; + md5 = "34eed507548117b2ab523ab14b2f8b55"; + }; + }; + werkzeug = super.buildPythonPackage { + name = "werkzeug-0.11.10"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/b7/7f/44d3cfe5a12ba002b253f6985a4477edfa66da53787a2a838a40f6415263/Werkzeug-0.11.10.tar.gz"; + md5 = "780967186f9157e88f2bfbfa6f07a893"; + }; + }; + +### Test requirements + + django = super.buildPythonPackage { + name = "django-1.9.7"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/50/76/aeb1bdde528b23e76df5964003e3e4e734c57c74e7358c3b2224987617dd/Django-1.9.7.tar.gz"; + md5 = "7de9ba83bfe01f4b7d45645c1b259c83"; + }; + }; + exam = super.buildPythonPackage { + name = "exam-0.10.6"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [mock]; + src = fetchurl { + url = "https://pypi.python.org/packages/c7/bd/c15ce029540bb1b551af83c0df502ba47e019ce7132a65db046ad16b8eda/exam-0.10.6.tar.gz"; + md5 = "0bf84acc2427a8a3d58d13d7297ff84a"; + }; + }; + mock = super.buildPythonPackage { + name = "mock-2.0.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [pbr six]; + src = fetchurl { + url = "https://pypi.python.org/packages/0c/53/014354fc93c591ccc4abff12c473ad565a2eb24dcd82490fae33dbf2539f/mock-2.0.0.tar.gz"; + md5 = "0febfafd14330c9dcaa40de2d82d40ad"; + }; + }; + nose = super.buildPythonPackage { + name = "nose-1.3.7"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz"; + md5 = "4d3ad0ff07b61373d2cefc89c5d0b20b"; + }; + }; + pbr = super.buildPythonPackage { + name = "pbr-1.10.0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/c3/2c/63275fab26a0fd8cadafca71a3623e4d0f0ee8ed7124a5bb128853d178a7/pbr-1.10.0.tar.gz"; + md5 = "8e4968c587268f030e38329feb9c8f17"; + }; + }; + redis = super.buildPythonPackage { + name = "redis-2.10.5"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/68/44/5efe9e98ad83ef5b742ce62a15bea609ed5a0d1caf35b79257ddb324031a/redis-2.10.5.tar.gz"; + md5 = "3b26c2b9703b4b56b30a1ad508e31083"; + }; + }; +} From b65327ab2cec5f6904267e3d09aec7eea7dc9fec Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:27:57 +0200 Subject: [PATCH 3/9] Add compat module for Python3 compatibility It provides a nice overview of the tweaks which we see in the codebase, so that it supports both Python2 and Python3. --- gutter/client/compat.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 gutter/client/compat.py diff --git a/gutter/client/compat.py b/gutter/client/compat.py new file mode 100644 index 0000000..a51f714 --- /dev/null +++ b/gutter/client/compat.py @@ -0,0 +1,22 @@ +""" +Python 3 compatibility. +""" + +import six + + +if six.PY3: + import unittest + ifilter = filter +else: + import unittest2 as unittest + from itertools import ifilter + + +NoneType = type(None) + + +class TestCaseMixin(object): + if six.PY3: + def assertItemsEqual(self, actual, expected, msg=None): + self.assertCountEqual(actual, expected, msg=None) From 190f70e043d73fead2ad2cf1f3854f5efffd38b8 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:30:51 +0200 Subject: [PATCH 4/9] Python3 conform catching of exceptions --- gutter/client/__init__.py | 2 +- tests/test_integration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gutter/client/__init__.py b/gutter/client/__init__.py index a2821ec..1b34b1a 100644 --- a/gutter/client/__init__.py +++ b/gutter/client/__init__.py @@ -8,7 +8,7 @@ try: VERSION = __import__('pkg_resources').get_distribution('gutter').version -except Exception, e: +except Exception as e: VERSION = 'unknown' CLIENT_CACHE = {} diff --git a/tests/test_integration.py b/tests/test_integration.py index 4843c26..ec321c5 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -446,5 +446,5 @@ def test_parent_switch_pickle_input(self): try: self.manager.active('new:switch') - except pickle.PicklingError, e: + except pickle.PicklingError as e: self.fail('Encountered pickling error: "%s"' % e) From 0f004148fc792e955abbe3fa6b82ac2ae2ad40be Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:34:20 +0200 Subject: [PATCH 5/9] Use six for string Py2/Py3 string type handling --- gutter/client/arguments/base.py | 6 ++++-- gutter/client/models.py | 10 ++++++---- gutter/client/testutils.py | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/gutter/client/arguments/base.py b/gutter/client/arguments/base.py index 1388c78..043ab11 100644 --- a/gutter/client/arguments/base.py +++ b/gutter/client/arguments/base.py @@ -1,4 +1,6 @@ -from types import NoneType +import six + +from gutter.client.compat import NoneType class classproperty(object): @@ -13,7 +15,7 @@ def __get__(self, instance, owner): class argument(object): def __init__(self, variable, getter): - if issubclass(type(getter), basestring): + if issubclass(type(getter), six.string_types): self.getter = lambda self: getattr(self.input, getter) else: self.getter = getter diff --git a/gutter/client/models.py b/gutter/client/models.py index f4750ec..a12c79e 100644 --- a/gutter/client/models.py +++ b/gutter/client/models.py @@ -12,10 +12,11 @@ import threading from collections import defaultdict from functools import partial -from itertools import ifilter # External Libraries +import six from gutter.client import signals +from gutter.client.compat import ifilter DEFAULT_SEPARATOR = ':' @@ -410,7 +411,7 @@ def __init__( if namespace is None: namespace = self.default_namespace - elif isinstance(namespace, basestring): + elif isinstance(namespace, six.string_types): namespace = [namespace] self.storage = storage @@ -440,7 +441,7 @@ def switches(self): List of all switches currently registered. """ results = [ - switch for name, switch in self.storage.iteritems() + switch for name, switch in six.iteritems(self.storage) if name.startswith(self.__joined_namespace) ] @@ -491,7 +492,8 @@ def register(self, switch, signal=signals.switch_registered): def unregister(self, switch_or_name): switch = getattr(switch_or_name, 'name', switch_or_name) - map(self.unregister, self.get_children(switch)) + for child in self.get_children(switch): + self.unregister(child) if switch in self: signals.switch_unregistered.call(self.switch(switch)) diff --git a/gutter/client/testutils.py b/gutter/client/testutils.py index 90fe4ea..dc40d4f 100644 --- a/gutter/client/testutils.py +++ b/gutter/client/testutils.py @@ -5,6 +5,7 @@ :copyright: (c) 2010-2012 DISQUS. :license: Apache License 2.0, see LICENSE for more details. """ +import six from functools import wraps from gutter.client import get_gutter_client @@ -39,7 +40,7 @@ def __init__(self, gutter=None, **keys): def gutter(self): if self._gutter is None: self._gutter = get_gutter_client() - elif isinstance(self._gutter, basestring): + elif isinstance(self._gutter, six.string_types): self._gutter = get_gutter_client(alias=self._gutter) return self._gutter From 6e3e3f668acd521018ab6196c0d223d3b4134277 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:34:40 +0200 Subject: [PATCH 6/9] Explicit relative imports in arguments --- gutter/client/arguments/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gutter/client/arguments/__init__.py b/gutter/client/arguments/__init__.py index b898fb5..317ea8b 100644 --- a/gutter/client/arguments/__init__.py +++ b/gutter/client/arguments/__init__.py @@ -1,7 +1,7 @@ from functools import partial -from base import Container, argument # noqa -import variables +from . import variables +from .base import Container, argument # noqa Value = partial(argument, variables.Value) Boolean = partial(argument, variables.Boolean) From ed92642a58a937de991765a91fca64b80e6d126e Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:36:12 +0200 Subject: [PATCH 7/9] Encode switch names into bytes Background idea is that we use string types as switch names and if we do this consistently, then the keys can be encoded into bytes objects. At least the RedisDict as storage will otherwise happily take str instances and return bytes instances. Result is that the Manager cannot find the switches anymore and we see issues due to mixing up str and bytes. --- gutter/client/encoding.py | 50 +++++++++++++++++++++++++++++++++++++-- tests/test_integration.py | 25 ++++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/gutter/client/encoding.py b/gutter/client/encoding.py index 7eea532..d53ecab 100644 --- a/gutter/client/encoding.py +++ b/gutter/client/encoding.py @@ -8,13 +8,59 @@ class JsonPickleEncoding(PickleEncoding): + + ENCODING = 'utf-8' + @staticmethod def encode(data): - return pickle.dumps(data) + json_string = pickle.dumps(data) + return json_string.encode(JsonPickleEncoding.ENCODING) @staticmethod def decode(data): try: - return pickle.loads(data) + json_string = data.decode(JsonPickleEncoding.ENCODING) + return pickle.loads(json_string) except Exception: return PickleEncoding.decode(data) + + +class KeyEncoderProxy: + """ + Helps to ensure that keys are encoded and decoded. + + This is mainly useful on Python3 where we use text values as the key but on + some storages like redis we can only store byte values. Without a + consistent encoding switches cannot be found again, because the returned + keys are `bytes` objects instead of `str` objects. + """ + + def __init__(self, storage, encoding='utf-8'): + self._encoding = encoding + self._storage = storage + + def __setitem__(self, key, val): + key = key.encode(self._encoding) + self._storage[key] = val + + def __delitem__(self, key): + key = key.encode(self._encoding) + del self._storage[key] + + def __getitem__(self, key): + key = key.encode(self._encoding) + return self._storage[key] + + def __contains__(self, key): + key = key.encode(self._encoding) + return key in self._storage + + def items(self): + return ( + (key.decode(self._encoding), val) + for key, val in self._storage.items()) + + def keys(self): + return ( + key.decode(self._encoding) + for key in self._storage.keys()) diff --git a/tests/test_integration.py b/tests/test_integration.py index ec321c5..db08926 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,18 +1,18 @@ -import unittest2 from nose.tools import * import zlib from redis import Redis from durabledict.redis import RedisDict -from gutter.client.encoding import JsonPickleEncoding +from gutter.client.encoding import KeyEncoderProxy, JsonPickleEncoding from gutter.client.operators.comparable import * from gutter.client.operators.identity import * from gutter.client.operators.misc import * from gutter.client.models import Switch, Condition, Manager from gutter.client import arguments from gutter.client import signals +from gutter.client.compat import unittest from exam.decorators import fixture, before, after, around from exam.cases import Exam @@ -26,7 +26,7 @@ class deterministicstring(str): """ def __hash__(self): - return zlib.crc32(self) + return zlib.crc32(self.encode('utf-8')) class User(object): @@ -61,7 +61,7 @@ class FloatArguments(arguments.Container): value = arguments.Integer(lambda self: self.input) -class TestIntegration(Exam, unittest2.TestCase): +class TestIntegration(Exam, unittest.TestCase): class Callback(object): def __init__(self): @@ -432,9 +432,24 @@ def flush_redis(self): @fixture def manager(self): - storage = RedisDict(keyspace='gutter-tests', connection=self.redis, encoding=JsonPickleEncoding) + storage = KeyEncoderProxy( + RedisDict( + keyspace='gutter-tests', + connection=self.redis, + encoding=JsonPickleEncoding)) return Manager(storage=storage) + def test_redis_dict_key_decoded(self): + storage = KeyEncoderProxy( + storage=RedisDict( + keyspace='test-encoding', + connection=self.redis, + encoding=JsonPickleEncoding)) + value = "Example string" + key = "Example key" + storage[key] = value + ok_(list(storage.keys()) == [key]) + def test_parent_switch_pickle_input(self): import pickle From 4e349853c7775591dacef01d2be8bd77ee94a1d4 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:38:20 +0200 Subject: [PATCH 8/9] Rich comparison handling for Python3 __cmp__ is ignored and cmp() is gone. Only one way left to compare. --- gutter/client/arguments/variables.py | 23 +++++++++++ gutter/client/models.py | 3 ++ tests/test_arguments.py | 57 ++++++++++++++++++---------- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/gutter/client/arguments/variables.py b/gutter/client/arguments/variables.py index 1d58660..bab0850 100644 --- a/gutter/client/arguments/variables.py +++ b/gutter/client/arguments/variables.py @@ -20,6 +20,15 @@ def func(self, *args, **kwargs): __hash__ = __proxy_to_value_method('__hash__') __nonzero__ = __proxy_to_value_method('__nonzero__') + # PY3 + __lt__ = __proxy_to_value_method('__lt__') + __le__ = __proxy_to_value_method('__le__') + __gt__ = __proxy_to_value_method('__gt__') + __ge__ = __proxy_to_value_method('__ge__') + __eq__ = __proxy_to_value_method('__eq__') + __ne__ = __proxy_to_value_method('__ne__') + __bool__ = __proxy_to_value_method('__bool__') + @staticmethod def to_python(value): return value @@ -62,9 +71,23 @@ class String(Base): def __cmp__(self, other): return cmp(self.value, other) + def __lt__(self, other): + return self.value < other + + def __gt__(self, other): + return self.value > other + + def __eq__(self, other): + return self.value == other + + def __hash__(self): + return hash(self.value) + def __nonzero__(self, *args, **kwargs): return bool(self.value) + __bool__ = __nonzero__ + @staticmethod def to_python(value): return str(value) diff --git a/gutter/client/models.py b/gutter/client/models.py index a12c79e..8acd9cf 100644 --- a/gutter/client/models.py +++ b/gutter/client/models.py @@ -331,6 +331,9 @@ def __eq__(self, other): self.negative is other.negative ) + # TODO: not sure if that's ok + __hash__ = object.__hash__ + def call(self, inpt): """ Returns if the condition applies to the ``inpt``. diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 58f0922..737ad38 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -1,10 +1,11 @@ -import unittest2 from mock import MagicMock, Mock from nose.tools import * +import six from gutter.client.arguments.variables import * from gutter.client import arguments from gutter.client.arguments import Container +from gutter.client.compat import unittest from exam.decorators import fixture @@ -15,7 +16,7 @@ class MyArguments(Container): str_variable = arguments.String('prop') -class TestBase(unittest2.TestCase): +class TestBase(unittest.TestCase): container = fixture(Container, True) subclass_arguments = fixture(MyArguments, True) @@ -65,7 +66,10 @@ def test_name_is_name_inside_class(self): class BaseVariableTest(object): - interface_functions = ['__cmp__', '__hash__', '__nonzero__'] + if six.PY2: + interface_functions = ['__cmp__', '__hash__', '__nonzero__'] + else: + interface_functions = ['__cmp__', '__hash__', '__bool__'] @fixture def argument(self): @@ -100,7 +104,7 @@ def test_delegates_all_interface_function_to_the_value_passed_in(self): values_function.assert_called_once_with(self.valid_comparison_value) -class ValueTest(BaseVariableTest, DelegateToValue, unittest2.TestCase): +class ValueTest(BaseVariableTest, DelegateToValue, unittest.TestCase): klass = Value valid_comparison_value = 'marv' @@ -110,11 +114,14 @@ def test_to_python_returns_same_object(self): eq_(Value.to_python(variable), variable) -class BooleanTest(BaseVariableTest, DelegateToValue, unittest2.TestCase): +class BooleanTest(BaseVariableTest, DelegateToValue, unittest.TestCase): klass = Boolean valid_comparison_value = True - interface_functions = ['__cmp__', '__nonzero__'] + if six.PY2: + interface_functions = ['__cmp__', '__nonzero__'] + else: + interface_functions = ['__lt__', '__gt__', '__eq__', '__bool__'] def test_hashes_its_hash_value_instead_of_value(self): boolean = Boolean(True, hash_value='another value') @@ -138,21 +145,33 @@ def test_to_python_booleans_the_value(self): eq_(Boolean.to_python('0'), True) -class StringTest(BaseVariableTest, DelegateToValue, unittest2.TestCase): +class StringTest(BaseVariableTest, DelegateToValue, unittest.TestCase): klass = String valid_comparison_value = 'foobazzle' - interface_functions = ['__hash__'] - - def test_cmp_compares_with_other_value(self): - eq_(self.argument.__cmp__('zebra'), -1) - eq_(self.argument.__cmp__('aardvark'), 1) - eq_(self.argument.__cmp__('foobazzle'), 0) - - def test_nonzero_returns_if_truthy(self): - ok_(String('hello').__nonzero__() is True) - ok_(String('').__nonzero__() is False) - ok_(String('0').__nonzero__() is True) + interface_functions = [] + + def test_compare_with_other_value(self): + ok_(self.argument < 'zebra') + ok_(self.argument > 'aardvark') + ok_(self.argument == 'foobazzle') + + if six.PY2: + def test_cmp_compares_with_other_value(self): + eq_(self.argument.__cmp__('zebra'), -1) + eq_(self.argument.__cmp__('aardvark'), 1) + eq_(self.argument.__cmp__('foobazzle'), 0) + + if six.PY2: + def test_nonzero_returns_if_truthy(self): + ok_(String('hello').__nonzero__() is True) + ok_(String('').__nonzero__() is False) + ok_(String('0').__nonzero__() is True) + else: + def test_bool_returns_if_truthy(self): + ok_(String('hello').__bool__() is True) + ok_(String('').__bool__() is False) + ok_(String('0').__bool__() is True) def test_to_python_strs_the_value(self): eq_(String.to_python(True), 'True') @@ -160,7 +179,7 @@ def test_to_python_strs_the_value(self): eq_(String.to_python(1), '1') -class IntegerTest(BaseVariableTest, DelegateToValue, unittest2.TestCase): +class IntegerTest(BaseVariableTest, DelegateToValue, unittest.TestCase): klass = Integer valid_comparison_value = 1 From 11bdbb95d9601b20d2578c8e996eb3f226b32f2b Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 26 Jun 2016 14:38:52 +0200 Subject: [PATCH 9/9] Use unittest via the compat module. --- tests/test_models.py | 23 ++++++++++++----------- tests/test_operators.py | 28 +++++++++++++++++----------- tests/test_registry.py | 9 +++++---- tests/test_signals.py | 14 +++++++------- tests/test_testutils.py | 4 ++-- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index df865a2..c222d84 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,5 @@ import itertools import threading -import unittest2 from nose.tools import * from gutter.client.arguments import Container as BaseArgument @@ -9,6 +8,7 @@ from durabledict import MemoryDict from durabledict.base import DurableDict from gutter.client import signals +from gutter.client.compat import unittest, TestCaseMixin import mock from exam.decorators import fixture, before from exam.cases import Exam @@ -41,7 +41,7 @@ class MOLArgument(BaseArgument): foo = arguments.Value(lambda self: 42) -class TestSwitch(ManagerMixin, unittest2.TestCase): +class TestSwitch(ManagerMixin, unittest.TestCase): possible_properties = [ ('state', (Switch.states.DISABLED, Switch.states.SELECTIVE)), ('compounded', (True, False)), @@ -178,7 +178,7 @@ def test_switches_are_still_equal_with_different_managers(self): eq_(a, b) -class TestSwitchChanges(ManagerMixin, unittest2.TestCase): +class TestSwitchChanges(ManagerMixin, unittest.TestCase): @fixture def switch(self): return Switch('foo') @@ -218,7 +218,7 @@ def test_switch_changes_returns_changes(self): ) -class TestCondition(unittest2.TestCase): +class TestCondition(unittest.TestCase): def argument_dict(name): return dict( module='module%s' % name, @@ -227,7 +227,7 @@ def argument_dict(name): ) possible_properties = [ - ('argument_dict', (argument_dict('1'), argument_dict('2'))), + ('argument', (argument_dict('1'), argument_dict('2'))), ('operator', ('o1', 'o2')), ('negative', (False, True)) ] @@ -296,6 +296,7 @@ def test_equals_if_has_the_same_properties(self): b = Condition(Argument, 'bar', bool) for prop, (a_val, b_val) in self.possible_properties: + print('PROP', prop, a_val, b_val) setattr(a, prop, a_val) setattr(b, prop, b_val) @@ -326,7 +327,7 @@ def pessamistic_condition(self): return mck -class ConcentTest(Exam, SwitchWithConditions, unittest2.TestCase): +class ConcentTest(Exam, SwitchWithConditions, unittest.TestCase): @fixture def manager(self): return Manager(storage=MemoryDict()) @@ -371,7 +372,7 @@ def test_without_concent_ignores_parents_enabled_status(self): eq_(self.switch.enabled_for('input'), False) -class DefaultConditionsTest(SwitchWithConditions, unittest2.TestCase): +class DefaultConditionsTest(SwitchWithConditions, unittest.TestCase): def test_enabled_for_is_true_if_any_conditions_are_true(self): ok_(self.switch.enabled_for('input') is False) self.switch.conditions[0].call.return_value = True @@ -389,7 +390,7 @@ def test_is_false_when_state_is_disabled(self): eq_(self.switch.enabled_for('input'), False) -class CompoundedConditionsTest(Exam, SwitchWithConditions, unittest2.TestCase): +class CompoundedConditionsTest(Exam, SwitchWithConditions, unittest.TestCase): @before def make_switch_compounded(self): self.switch.compounded = True @@ -402,7 +403,7 @@ def test_enabled_if_all_conditions_are_true(self): ok_(self.switch.enabled_for('input') is True) -class ManagerTest(unittest2.TestCase): +class ManagerTest(unittest.TestCase, TestCaseMixin): storage_with_existing_switches = { 'default.existing': 'switch', 'default.another': 'valuable switch' @@ -664,7 +665,7 @@ def test_register_signals_switch_registered_with_switch(self, signal): signal.call.assert_called_once_with(switch) -class EmptyManagerInstanceTest(ActsLikeManager, unittest2.TestCase): +class EmptyManagerInstanceTest(ActsLikeManager, unittest.TestCase): def test_input_accepts_variable_input_args(self): eq_(self.manager.inputs, []) self.manager.input('input1', 'input2') @@ -697,7 +698,7 @@ def manager(self): return Manager(storage=MemoryDict(), namespace=['a', 'b']) -class ManagerWithInputTest(Exam, ActsLikeManager, unittest2.TestCase): +class ManagerWithInputTest(Exam, ActsLikeManager, unittest.TestCase): def build_and_register_switch(self, name, enabled_for=False): switch = Switch(name) switch.enabled_for = mock.Mock(return_value=enabled_for) diff --git a/tests/test_operators.py b/tests/test_operators.py index cd244e3..f1a40dc 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -1,11 +1,12 @@ -import unittest2 from nose.tools import * # noqa +import six from gutter.client.operators import OperatorInitError from gutter.client.operators.comparable import * # noqa from gutter.client.operators.identity import * # noqa from gutter.client.operators.misc import * # noqa from gutter.client.operators.string import * # noqa +from gutter.client.compat import unittest from exam.decorators import fixture @@ -43,7 +44,7 @@ def property_class(self): return type(self.operator) -class TestTruthyCondition(BaseOperator, unittest2.TestCase): +class TestTruthyCondition(BaseOperator, unittest.TestCase): def make_operator(self): return Truthy() @@ -64,7 +65,7 @@ def test_arguments_is_empty_list(self): eq_(self.operator.arguments, ()) -class TestEqualsCondition(BaseOperator, unittest2.TestCase): +class TestEqualsCondition(BaseOperator, unittest.TestCase): def make_operator(self): return Equals(value='Fred') @@ -109,7 +110,7 @@ def test_str_says_is_equal_to_condition(self): eq_(self.str, 'strip ignore case equal to "fred"') -class TestBetweenCondition(BaseOperator, unittest2.TestCase): +class TestBetweenCondition(BaseOperator, unittest.TestCase): def make_operator(self, lower=1, higher=100): return Between(lower_limit=lower, upper_limit=higher) @@ -120,7 +121,12 @@ def test_applies_to_if_between_lower_and_upper_bound(self): ok_(self.operator.applies_to(2)) ok_(self.operator.applies_to(99)) ok_(self.operator.applies_to(100) is False) - ok_(self.operator.applies_to('steve') is False) + + if six.PY2: + # TODO: Py3 raises a TypeError, which seems to be the right thing + # to do, since comparing a string to an integer range is most + # probably a programming mistake and should not go unnoticed. + ok_(self.operator.applies_to('steve') is False) def test_applies_to_works_with_any_comparable(self): animals = Between(lower_limit='cobra', upper_limit='orangatang') @@ -138,7 +144,7 @@ def test_variables_is_just_a_lower_and_higher(self): eq_(self.operator.variables, dict(lower_limit=1, upper_limit=100)) -class TestLessThanCondition(BaseOperator, unittest2.TestCase): +class TestLessThanCondition(BaseOperator, unittest.TestCase): def make_operator(self, upper=500): return LessThan(upper_limit=upper) @@ -200,7 +206,7 @@ def test_variables_is_upper_limit(self): eq_(self.operator.variables, dict(upper_limit=500)) -class TestMoreThanOperator(BaseOperator, unittest2.TestCase): +class TestMoreThanOperator(BaseOperator, unittest.TestCase): def make_operator(self, lower=10): return MoreThan(lower_limit=lower) @@ -229,7 +235,7 @@ def test_variables_is_lower_limit(self): eq_(self.operator.variables, dict(lower_limit=10)) -class TestMoreThanOrEqualToOperator(BaseOperator, unittest2.TestCase): +class TestMoreThanOrEqualToOperator(BaseOperator, unittest.TestCase): def make_operator(self, lower=10): return MoreThanOrEqualTo(lower_limit=lower) @@ -269,10 +275,10 @@ def __nonzero__(self): def successful_runs(self, number): runs = map(self.operator.applies_to, range(1000)) - return len(filter(bool, runs)) + return len(list(filter(bool, runs))) -class PercentageTest(PercentTest, unittest2.TestCase): +class PercentageTest(PercentTest, unittest.TestCase): def make_operator(self): return Percent(percentage=50) @@ -287,7 +293,7 @@ def test_variables_is_percentage(self): eq_(self.operator.variables, dict(percentage=50)) -class PercentRangeTest(PercentTest, unittest2.TestCase): +class PercentRangeTest(PercentTest, unittest.TestCase): def make_operator(self): return self.range_of(10, 20) diff --git a/tests/test_registry.py b/tests/test_registry.py index f85f22f..26bd336 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,5 +1,4 @@ import itertools -import unittest2 from copy import copy from exam.cases import Exam @@ -13,9 +12,11 @@ ) from gutter.client.arguments.base import Container from gutter.client import registry +from gutter.client.compat import unittest + def all_operators_in(module): - for _, obj in vars(module).iteritems(): + for _, obj in vars(module).items(): try: if issubclass(obj, Base) and obj is not Base: yield obj @@ -36,7 +37,7 @@ class TestArgument(Container): pass -class TestOperatorRegistry(Exam, unittest2.TestCase): +class TestOperatorRegistry(Exam, unittest.TestCase): @around def preserve_registry(self): @@ -66,7 +67,7 @@ def test_raises_exception_if_object_is_not_an_operator(self): ) -class TestArgumentRegistry(Exam, unittest2.TestCase): +class TestArgumentRegistry(Exam, unittest.TestCase): @around def preserve_registry(self): diff --git a/tests/test_signals.py b/tests/test_signals.py index f83c3fd..05e7ae2 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,6 +1,6 @@ -import unittest2 from nose.tools import * from gutter.client import signals +from gutter.client.compat import unittest import mock from exam.decorators import fixture @@ -32,42 +32,42 @@ def test_signal_passes_args_along_to_callback(self): self.callback.assert_called_once_with(1, 2.0, kw='args') -class TestSwitchRegisteredCallback(ActsLikeSignal, unittest2.TestCase): +class TestSwitchRegisteredCallback(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): return signals.switch_registered -class TestSwitchUnregisteredCallback(ActsLikeSignal, unittest2.TestCase): +class TestSwitchUnregisteredCallback(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): return signals.switch_unregistered -class TestSwitchUpdatedCallback(ActsLikeSignal, unittest2.TestCase): +class TestSwitchUpdatedCallback(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): return signals.switch_updated -class TestConditionApplyErrorCallback(ActsLikeSignal, unittest2.TestCase): +class TestConditionApplyErrorCallback(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): return signals.switch_updated -class TestSwitchChecked(ActsLikeSignal, unittest2.TestCase): +class TestSwitchChecked(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): return signals.switch_checked -class TestSwitchActive(ActsLikeSignal, unittest2.TestCase): +class TestSwitchActive(ActsLikeSignal, unittest.TestCase): @fixture def signal(self): diff --git a/tests/test_testutils.py b/tests/test_testutils.py index 7e37767..3492026 100644 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -1,8 +1,8 @@ -import unittest2 from durabledict import MemoryDict from nose.tools import * from gutter.client import get_gutter_client +from gutter.client.compat import unittest from gutter.client.encoding import JsonPickleEncoding from gutter.client.testutils import switches from gutter.client.models import Switch @@ -11,7 +11,7 @@ from exam.cases import Exam -class TestDecorator(Exam, unittest2.TestCase): +class TestDecorator(Exam, unittest.TestCase): @fixture def gutter(self):