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/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/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) 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/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/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) 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/gutter/client/models.py b/gutter/client/models.py index f4750ec..8acd9cf 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 = ':' @@ -330,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``. @@ -410,7 +414,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 +444,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 +495,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 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"; + }; + }; +} 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'], 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 diff --git a/tests/test_integration.py b/tests/test_integration.py index 4843c26..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 @@ -446,5 +461,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) 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):