From f550077a1eab128f37f0ca4d9a39cdcc3c0330a2 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:14:33 -0700 Subject: [PATCH 01/18] pylint: disable E1101 This error is too much of a squeaky wheel in this module. --- tests/richenum/test_ordered_rich_enums.py | 13 ++++++++----- tests/richenum/test_rich_enums.py | 22 ++++++++++++---------- tests/richenum/test_simple_enums.py | 2 ++ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/richenum/test_ordered_rich_enums.py b/tests/richenum/test_ordered_rich_enums.py index 22e06f5..5be05f2 100644 --- a/tests/richenum/test_ordered_rich_enums.py +++ b/tests/richenum/test_ordered_rich_enums.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- + +# pylint: disable=E1101 + import copy import unittest2 as unittest @@ -32,16 +35,16 @@ class SadBreakfast(OrderedRichEnum): class OrderedRichEnumTestSuite(unittest.TestCase): def test_lookup_by_index(self): - self.assertEqual(Breakfast.from_index(0), coffee) # pylint: disable=E1101 + self.assertEqual(Breakfast.from_index(0), coffee) # Should work if enum isn't zero-indexed. - self.assertEqual(SadBreakfast.from_index(1), oatmeal) # pylint: disable=E1101 + self.assertEqual(SadBreakfast.from_index(1), oatmeal) with self.assertRaises(EnumLookupError): - SadBreakfast.from_index(7) # pylint: disable=E1101 + SadBreakfast.from_index(7) def test_construction_preserves_indices(self): - self.assertEqual(SadBreakfast.OATMEAL.index, 1) # pylint: disable=E1101 - self.assertEqual(Breakfast.OATMEAL.index, 1) # pylint: disable=E1101 + self.assertEqual(SadBreakfast.OATMEAL.index, 1) + self.assertEqual(Breakfast.OATMEAL.index, 1) def test_cannot_have_duplicate_indices(self): with self.assertRaisesRegexp(EnumConstructionException, 'Index already defined'): diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index a69af3c..8775b08 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: disable=E1101 + import copy import unittest2 as unittest @@ -33,7 +35,7 @@ def test_members_are_accessible_through_attributes(self): self.assertEqual(Vegetable.BROCCOLI, broccoli) with self.assertRaises(AttributeError): - Vegetable.PARSNIP # pylint: disable=E1101 + Vegetable.PARSNIP def test_membership(self): self.assertTrue(Vegetable.OKRA in Vegetable) @@ -50,27 +52,27 @@ def test_enums_iterate_through_members(self): self.assertEqual(members, (Vegetable.OKRA, Vegetable.BROCCOLI)) def test_lookup_by_canonical_name(self): - self.assertEqual(Vegetable.from_canonical('okra'), Vegetable.OKRA) # pylint: disable=E1101 + self.assertEqual(Vegetable.from_canonical('okra'), Vegetable.OKRA) with self.assertRaises(EnumLookupError): - Vegetable.from_canonical('parsnip') # pylint: disable=E1101 + Vegetable.from_canonical('parsnip') def test_lookup_by_display_name(self): - self.assertEqual(Vegetable.from_display('Okra'), Vegetable.OKRA) # pylint: disable=E1101 + self.assertEqual(Vegetable.from_display('Okra'), Vegetable.OKRA) with self.assertRaises(EnumLookupError): - Vegetable.from_display('Parsnip') # pylint: disable=E1101 + Vegetable.from_display('Parsnip') def test_generic_lookup(self): - self.assertEqual(Vegetable.lookup('canonical_name', 'okra'), Vegetable.OKRA) # pylint: disable=E1101 - self.assertEqual(Vegetable.lookup('flavor', 'gross'), Vegetable.OKRA) # pylint: disable=E1101 + self.assertEqual(Vegetable.lookup('canonical_name', 'okra'), Vegetable.OKRA) + self.assertEqual(Vegetable.lookup('flavor', 'gross'), Vegetable.OKRA) with self.assertRaises(EnumLookupError): - Vegetable.lookup('flavor', 'yum') # pylint: disable=E1101 + Vegetable.lookup('flavor', 'yum') def test_choices(self): self.assertEqual( - Vegetable.choices(), # pylint: disable=E1101 + Vegetable.choices(), [('okra', 'Okra'), ('broccoli', 'Broccoli')]) self.assertEqual( - Vegetable.choices(value_field='flavor', display_field='canonical_name'), # pylint: disable=E1101 + Vegetable.choices(value_field='flavor', display_field='canonical_name'), [('gross', 'okra'), ('delicious', 'broccoli')]) def test_public_members_must_be_enum_values(self): diff --git a/tests/richenum/test_simple_enums.py b/tests/richenum/test_simple_enums.py index 217a2a5..a4b13f1 100644 --- a/tests/richenum/test_simple_enums.py +++ b/tests/richenum/test_simple_enums.py @@ -1,3 +1,5 @@ +# pylint: disable=E1101 + import unittest2 as unittest from richenum import enum From f511229fa834abdeff79cba633e6efd603872b7b Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:08:13 -0700 Subject: [PATCH 02/18] Add a tox.ini for multi-version testing --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0d35c46 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py27,py33,pypy +[testenv] +deps = setuptools +commands = python setup.py test From 18e8002d6cb289cd76ab3000e4fe631f6d7dd440 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:09:21 -0700 Subject: [PATCH 03/18] python 3: use try: except ImportError work-around for 'unittest2' --- setup.py | 9 ++++++--- tests/richenum/test_ordered_rich_enums.py | 5 ++++- tests/richenum/test_rich_enums.py | 5 ++++- tests/richenum/test_simple_enums.py | 5 ++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index bb249df..58c88ee 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,14 @@ #!/usr/bin/env python +import sys + from setuptools import setup, find_packages -tests_require = [ - 'unittest2', -] +tests_require = [] + +if sys.version_info.major == 2: + tests_require.append("unittest2") setup( diff --git a/tests/richenum/test_ordered_rich_enums.py b/tests/richenum/test_ordered_rich_enums.py index 5be05f2..d9f6265 100644 --- a/tests/richenum/test_ordered_rich_enums.py +++ b/tests/richenum/test_ordered_rich_enums.py @@ -3,7 +3,10 @@ # pylint: disable=E1101 import copy -import unittest2 as unittest +try: + import unittest2 as unittest +except ImportError: + import unittest from richenum import EnumConstructionException from richenum import EnumLookupError diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index 8775b08..bf1e6b2 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -2,7 +2,10 @@ # pylint: disable=E1101 import copy -import unittest2 as unittest +try: + import unittest2 as unittest +except ImportError: + import unittest from richenum import EnumConstructionException from richenum import EnumLookupError diff --git a/tests/richenum/test_simple_enums.py b/tests/richenum/test_simple_enums.py index a4b13f1..c99923a 100644 --- a/tests/richenum/test_simple_enums.py +++ b/tests/richenum/test_simple_enums.py @@ -1,6 +1,9 @@ # pylint: disable=E1101 -import unittest2 as unittest +try: + import unittest2 as unittest +except ImportError: + import unittest from richenum import enum from richenum import EnumConstructionException From 3383c1b87175ad638f78030da1660a8ca0207ce7 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:10:35 -0700 Subject: [PATCH 04/18] simplify a test No need to catch the exception and raise a more helpful error; simply annotate the test instead. --- tests/richenum/test_simple_enums.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/richenum/test_simple_enums.py b/tests/richenum/test_simple_enums.py index c99923a..8261e5b 100644 --- a/tests/richenum/test_simple_enums.py +++ b/tests/richenum/test_simple_enums.py @@ -36,11 +36,9 @@ def test_choices_are_ordered_by_value(self): self.assertEqual(Shuffled.choices, Breakfast.choices) def test_values_can_be_any_hashable_type(self): - try: - Confused = enum(INT=0, TUPLE=(1, 2), STR='yup') - self.assertEqual(Confused.get_id_by_label('TUPLE'), (1, 2)) - except: - self.fail('Simple enums should accept values of any hashable type.') + """Test simple enums accept values of any hashable type""" + Confused = enum(INT=0, TUPLE=(1, 2), STR='yup') + self.assertEqual(Confused.get_id_by_label('TUPLE'), (1, 2)) with self.assertRaisesRegexp(EnumConstructionException, 'hashable'): Confused = enum(LIST=[1, 2]) From 2194c5b2a9ca09f9e9e0c2a5764446e4b94d2fb7 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:26:31 -0700 Subject: [PATCH 05/18] python 3: unicode vs str See python3porting.com :) --- src/richenum/enums.py | 27 +++++++++++++++++------ tests/richenum/test_ordered_rich_enums.py | 11 ++++++--- tests/richenum/test_rich_enums.py | 11 ++++++--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index fc65cb7..23982af 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -2,6 +2,8 @@ import copy import logging import new +from six import PY3 +from six import string_types from operator import itemgetter @@ -23,6 +25,15 @@ class EnumLookupError(Exception): pass +def _str_or_ascii_replace(stringy): + if PY3: + return stringy + else: + if isinstance(stringy, str): + stringy = stringy.decode('utf-8', 'replace') + return stringy.encode('ascii', 'replace') + + def enum(**enums): """ A basic enum implementation. @@ -58,14 +69,16 @@ def __init__(self, canonical_name, display_name, *args, **kwargs): def __repr__(self): return "<%s: %s ('%s')>" % ( self.__class__.__name__, - self.canonical_name.decode('utf-8', 'replace').encode('ascii', 'replace'), - unicode(self.display_name).encode('ascii', 'replace')) + _str_or_ascii_replace(self.canonical_name), + _str_or_ascii_replace(self.display_name), + ) def __unicode__(self): return unicode(self.display_name) def __str__(self): - return unicode(self).encode('utf-8', 'xmlcharrefreplace') + return self.display_name if PY3 else unicode(self).encode( + 'utf-8', 'xmlcharrefreplace') def __hash__(self): return hash(self.canonical_name) @@ -111,8 +124,9 @@ def __repr__(self): return "<%s #%s: %s ('%s')>" % ( self.__class__.__name__, self.index, - self.canonical_name.decode('utf-8', 'replace').encode('ascii', 'replace'), - unicode(self.display_name).encode('ascii', 'replace')) + _str_or_ascii_replace(self.canonical_name), + _str_or_ascii_replace(self.display_name), + ) def __cmp__(self, other): if other is None: @@ -228,8 +242,7 @@ def lookup(cls, field, value): return member if ( - not isinstance(member_value, str) and - not isinstance(member_value, unicode) and + not isinstance(member_value, string_types) and isinstance(member_value, collections.Iterable) and value in member_value ): diff --git a/tests/richenum/test_ordered_rich_enums.py b/tests/richenum/test_ordered_rich_enums.py index d9f6265..1951c36 100644 --- a/tests/richenum/test_ordered_rich_enums.py +++ b/tests/richenum/test_ordered_rich_enums.py @@ -3,6 +3,7 @@ # pylint: disable=E1101 import copy +from six import PY3 try: import unittest2 as unittest except ImportError: @@ -111,7 +112,11 @@ def test_equality_by_index_and_type(self): self.assertEqual(Breakfast.COFFEE, coffee_copy) def test_unicode_handling(self): - poop_oatmeal = BreakfastEnumValue(3, 'oatmeal💩', u'Oatmeal💩') - self.assertEqual(repr(poop_oatmeal), "") + poop_oatmeal = BreakfastEnumValue(3, u'oatmeal💩', u'Oatmeal💩') + self.assertRegexpMatches( + repr(poop_oatmeal), + r"", + ) self.assertEqual(str(poop_oatmeal), "Oatmeal💩") - self.assertEqual(unicode(poop_oatmeal), u"Oatmeal💩") + if not PY3: + self.assertEqual(unicode(poop_oatmeal), u"Oatmeal💩") diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index bf1e6b2..e7c001f 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -2,6 +2,7 @@ # pylint: disable=E1101 import copy +from six import PY3 try: import unittest2 as unittest except ImportError: @@ -133,7 +134,11 @@ def test_equality_by_canonical_name_and_type(self): self.assertEqual(Vegetable.OKRA, okra_copy) def test_unicode_handling(self): - poop_okra = VegetableEnumValue('gross', 'okra💩', u'Okra💩') - self.assertEqual(repr(poop_okra), "") + poop_okra = VegetableEnumValue('gross', u'okra💩', u'Okra💩') + self.assertRegexpMatches( + repr(poop_okra), + "", + ) self.assertEqual(str(poop_okra), "Okra💩") - self.assertEqual(unicode(poop_okra), u"Okra💩") + if not PY3: + self.assertEqual(unicode(poop_okra), u"Okra💩") From 599c19acf86c432a191b391a0ec6d0d20b0c427c Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:17:21 -0700 Subject: [PATCH 06/18] python 3: no more dict.itervalues() Replace with a helper (2to3 is too expensive!) --- src/richenum/enums.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 23982af..4b47b90 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -34,6 +34,20 @@ def _str_or_ascii_replace(stringy): return stringy.encode('ascii', 'replace') +def _items(dict): + try: + return dict.iteritems() + except AttributeError: + return dict.items() + + +def _values(dict): + try: + return dict.itervalues() + except AttributeError: + return dict.values() + + def enum(**enums): """ A basic enum implementation. @@ -46,7 +60,7 @@ def enum(**enums): 2 """ # Enum values must be hashable to support reverse lookup. - if not all(isinstance(val, collections.Hashable) for val in enums.itervalues()): + if not all(isinstance(val, collections.Hashable) for val in _values(enums)): raise EnumConstructionException('All enum values must be hashable.') # Cheating by maintaining a copy of original dict for iteration b/c iterators are hard. @@ -54,9 +68,9 @@ def enum(**enums): en = copy.deepcopy(enums) e = new.classobj('Enum', (), enums) e._dict = en - e.choices = [(v, k) for k, v in sorted(en.iteritems(), key=itemgetter(1))] # DEPRECATED + e.choices = [(v, k) for k, v in sorted(_items(en), key=itemgetter(1))] # DEPRECATED e.get_id_by_label = e._dict.get - e.get_label_by_id = dict([(v, k) for (k, v) in e._dict.items()]).get + e.get_label_by_id = dict([(v, k) for (k, v) in _items(e._dict)]).get return e From 68fdfa311ea81e2f7e2219411421f1c9dd3c924a Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:15:31 -0700 Subject: [PATCH 07/18] python 3: no more 'new'; port to type(x, y, z) instead --- src/richenum/enums.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 4b47b90..6963399 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -1,7 +1,6 @@ import collections import copy import logging -import new from six import PY3 from six import string_types @@ -66,11 +65,11 @@ def enum(**enums): # Cheating by maintaining a copy of original dict for iteration b/c iterators are hard. # It must be a deepcopy because new.classobj() modifies the original. en = copy.deepcopy(enums) - e = new.classobj('Enum', (), enums) - e._dict = en - e.choices = [(v, k) for k, v in sorted(_items(en), key=itemgetter(1))] # DEPRECATED - e.get_id_by_label = e._dict.get - e.get_label_by_id = dict([(v, k) for (k, v) in _items(e._dict)]).get + e = type('Enum', (_EnumMethods,), dict((k, v) for k, v in _items(en))) + + e.choices = [(v, k) for k, v in sorted(en.iteritems(), key=itemgetter(1))] # DEPRECATED + e.get_id_by_label = e.__dict__.get + e.get_label_by_id = dict((v, k) for (k, v) in _items(enums)).get return e From 98cef9062adcf97f5afe7d9a99398a95ff838ef3 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:16:16 -0700 Subject: [PATCH 08/18] python 3: replace 'long' with numbers.Integral --- src/richenum/enums.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 6963399..353d9f7 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -1,6 +1,7 @@ import collections import copy import logging +import numbers from six import PY3 from six import string_types @@ -126,7 +127,7 @@ def choicify(self, value_field="canonical_name", display_field="display_name"): class OrderedRichEnumValue(RichEnumValue): def __init__(self, index, canonical_name, display_name, *args, **kwargs): super(OrderedRichEnumValue, self).__init__(canonical_name, display_name, args, kwargs) - if not isinstance(index, (int, long)): + if not isinstance(index, numbers.Integral): raise EnumConstructionException("Index must be an integer type, not: %s" % type(index)) if index < 0: raise EnumConstructionException("Index cannot be a negative number") From df3036465c5d5130f71aeb5ae1c8eea6c7affd42 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:19:44 -0700 Subject: [PATCH 09/18] python 3: mismatched types might not compare safely The test which sets up an enum with integers and tuples as values fails here in this deprecated block because the values don't compare. Just skip that if it happens; it's probably a silly case anyway. --- src/richenum/enums.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 353d9f7..abc1c27 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -68,7 +68,10 @@ def enum(**enums): en = copy.deepcopy(enums) e = type('Enum', (_EnumMethods,), dict((k, v) for k, v in _items(en))) - e.choices = [(v, k) for k, v in sorted(en.iteritems(), key=itemgetter(1))] # DEPRECATED + try: + e.choices = [(v, k) for k, v in sorted(_items(enums), key=itemgetter(1))] # DEPRECATED + except TypeError: + pass e.get_id_by_label = e.__dict__.get e.get_label_by_id = dict((v, k) for (k, v) in _items(enums)).get From 00c98b7902aeee6d5d26aaefbc4e21cc014d964e Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:22:03 -0700 Subject: [PATCH 10/18] python 3: metaclass support Use the 'six' with_metaclass helper. The exception must handle a new type of virtual base class; make it generic while we're here. ie, a 'RichEnum' sub-class can define __virtual__ to suppress this exception. --- src/richenum/enums.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index abc1c27..7e72d0f 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -4,6 +4,7 @@ import numbers from six import PY3 from six import string_types +from six import with_metaclass from operator import itemgetter @@ -187,7 +188,9 @@ def _setup_members(cls_attrs, cls_parents, member_cls): else: last_type = attr_type - if cls_parents not in [(object, ), (_EnumMethods, )] and not members: + if "__virtual__" not in cls_attrs and cls_parents not in [ + (object, ), (_EnumMethods, ) + ] and not members: raise EnumConstructionException( "Must specify at least one attribute when using RichEnum") @@ -270,11 +273,11 @@ def lookup(cls, field, value): @classmethod def from_canonical(cls, canonical_name): - return cls.lookup('canonical_name', canonical_name) + return cls.lookup('canonical_name', canonical_name) # pylint: disable=E1101 @classmethod def from_display(cls, display_name): - return cls.lookup('display_name', display_name) + return cls.lookup('display_name', display_name) # pylint: disable=E1101 @classmethod def choices(cls, value_field='canonical_name', display_field='display_name'): @@ -291,7 +294,7 @@ def choices(cls, value_field='canonical_name', display_field='display_name'): return [m.choicify(value_field=value_field, display_field=display_field) for m in cls.members()] -class RichEnum(_EnumMethods): +class RichEnum(with_metaclass(_RichEnumMetaclass, _EnumMethods)): """ Enumeration that can represent a name for referencing (canonical_name) and a name for displaying (display_name). @@ -316,10 +319,10 @@ class RichEnum(_EnumMethods): easier to differentiate between all of your different RichEnums. """ - __metaclass__ = _RichEnumMetaclass + __virtual__ = True -class OrderedRichEnum(_EnumMethods): +class OrderedRichEnum(with_metaclass(_OrderedRichEnumMetaclass, _EnumMethods)): """ Use OrderedRichEnum when you need a RichEnum with index-based access into the enum, e.g. OrderedRichEnumExample.from_index(0), @@ -341,8 +344,8 @@ class OrderedRichEnum(_EnumMethods): >>> MyOrderedRichEnum.from_index(1) OrderedRichEnumValue - idx: 1 canonical_name: 'foo' display_name: 'Foo' """ - __metaclass__ = _OrderedRichEnumMetaclass + __virtual__ = True @classmethod def from_index(cls, index): - return cls.lookup('index', index) + return cls.lookup('index', index) # pylint: disable=E1101 From 11f508afda71ef52f2fabadccda26ea1d1e0de27 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:23:47 -0700 Subject: [PATCH 11/18] python 3: replace __cmp__ with a __lt__ The total_ordering wrapper is a no-op on python 3. --- src/richenum/enums.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 7e72d0f..01edf20 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -1,5 +1,6 @@ import collections import copy +from functools import total_ordering import logging import numbers from six import PY3 @@ -79,6 +80,7 @@ def enum(**enums): return e +@total_ordering class RichEnumValue(object): def __init__(self, canonical_name, display_name, *args, **kwargs): self.canonical_name = canonical_name @@ -101,12 +103,12 @@ def __str__(self): def __hash__(self): return hash(self.canonical_name) - def __cmp__(self, other): + def __lt__(self, other): if other is None: return -1 if not isinstance(other, type(self)): return -1 - return cmp(self.canonical_name, other.canonical_name) + return self.canonical_name < other.canonical_name def __eq__(self, other): if other is None: @@ -128,6 +130,7 @@ def choicify(self, value_field="canonical_name", display_field="display_name"): return (getattr(self, value_field), getattr(self, display_field)) +@total_ordering class OrderedRichEnumValue(RichEnumValue): def __init__(self, index, canonical_name, display_name, *args, **kwargs): super(OrderedRichEnumValue, self).__init__(canonical_name, display_name, args, kwargs) @@ -146,19 +149,17 @@ def __repr__(self): _str_or_ascii_replace(self.display_name), ) - def __cmp__(self, other): - if other is None: - return -1 - if not isinstance(other, type(self)): - return -1 - return cmp(self.index, other.index) + def __lt__(self, other): + if isinstance(other, type(self)): + return self.index < other.index + else: + return True def __eq__(self, other): - if other is None: - return False - if not isinstance(other, type(self)): + if isinstance(other, type(self)): + return self.index == other.index + else: return False - return self.index == other.index def _setup_members(cls_attrs, cls_parents, member_cls): From 9fe726b4d6f3397d9f2ae2f113e515baebcf470a Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Wed, 16 Apr 2014 23:35:59 -0700 Subject: [PATCH 12/18] Drop deprecated property 'choices' strict ordering requirement This was sporadic on Python 3 and a deprecated field, so just make sure all fields are present. --- tests/richenum/test_rich_enums.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index e7c001f..7bd16a7 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -52,8 +52,8 @@ def test_membership(self): self.assertFalse(parsnip in Vegetable) def test_enums_iterate_through_members(self): - members = tuple(e for e in Vegetable) - self.assertEqual(members, (Vegetable.OKRA, Vegetable.BROCCOLI)) + members = set(e for e in Vegetable) + self.assertEqual(members, set((Vegetable.OKRA, Vegetable.BROCCOLI))) def test_lookup_by_canonical_name(self): self.assertEqual(Vegetable.from_canonical('okra'), Vegetable.OKRA) @@ -73,11 +73,14 @@ def test_generic_lookup(self): def test_choices(self): self.assertEqual( - Vegetable.choices(), - [('okra', 'Okra'), ('broccoli', 'Broccoli')]) + set(x for x in Vegetable.choices()), + set((('okra', 'Okra'), ('broccoli', 'Broccoli'))), + ) + self.assertEqual( - Vegetable.choices(value_field='flavor', display_field='canonical_name'), - [('gross', 'okra'), ('delicious', 'broccoli')]) + set(x for x in Vegetable.choices(value_field='flavor', display_field='canonical_name')), + set((('gross', 'okra'), ('delicious', 'broccoli'))), + ) def test_public_members_must_be_enum_values(self): with self.assertRaisesRegexp(EnumConstructionException, 'Invalid attribute'): From 4ffdd37d0b130cd9ba4ef4746ed0edcf0c52c27c Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:31:51 -0700 Subject: [PATCH 13/18] Release engineering for 1.1.0 Changelog, add trove classifiers to setup.py, and complete the tox.ini --- .travis.yml | 6 +++--- AUTHORS.rst | 1 + CHANGELOG.rst | 4 ++++ README.rst | 5 +++++ setup.py | 16 +++++++++++++++- tox.ini | 8 +++++--- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f85afb..159510f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,13 @@ language: python python: - "2.6" - "2.7" + - "3.3" install: # pylint needs unittest2 to check the test code. - - "pip install -q flake8 pylint unittest2 --use-mirrors" - - "pip install -e . --use-mirrors" + - "pip install -q flake8 pylint unittest2 nose six --use-mirrors" before_script: - "flake8 tests src setup.py" - "pylint -E src/richenum" - "pylint -E tests/richenum" - "pylint -E setup.py" -script: "python setup.py test" +script: "nosetests -s -v" diff --git a/AUTHORS.rst b/AUTHORS.rst index 377551b..3695393 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,3 +7,4 @@ Contributors | `Akshay Shah `_ | `Dale Hui `_ | `Robert MacCloy `_ +| `Sam Vilain `_ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a59c22..ae6cb4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Changelog ========= +1.1.0 (2014-04-17) +------------------ + - support for Python 3 and PyPy + 1.0.4 (2013-12-03) ------------------ - Better unicode handling in ``__str__``, ``__unicode__``, and diff --git a/README.rst b/README.rst index b6ba524..ffb1c52 100644 --- a/README.rst +++ b/README.rst @@ -99,6 +99,11 @@ django-richenum | `PyPi `_ +enum + Starting with Python 3.4, there is a standard library for enumerations. + This class has a similar API, but is not directly compatible with that + class. + ============ Contributing diff --git a/setup.py b/setup.py index 58c88ee..83044e8 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,20 @@ open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read() + '\n\n' + open('AUTHORS.rst').read()), - classifiers=[], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], keywords='python enum richenum', url='https://github.com/hearsaycorp/richenum', author='Hearsay Social', @@ -28,5 +41,6 @@ package_dir={'': 'src'}, packages=find_packages('src'), tests_require=tests_require, + install_requires=['six'], test_suite='tests' ) diff --git a/tox.ini b/tox.ini index 0d35c46..eea4b9f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] -envlist = py27,py33,pypy +envlist = py26,py27,py33,py34,pypy [testenv] -deps = setuptools -commands = python setup.py test +deps = + nose + six +commands = nosetests From e54767ba6187db688268ccef717cc213788b0df1 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:43:30 -0700 Subject: [PATCH 14/18] travis supports pypy too, apparently --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 159510f..4651f70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.6" - "2.7" - "3.3" + - "pypy" install: # pylint needs unittest2 to check the test code. - "pip install -q flake8 pylint unittest2 nose six --use-mirrors" From 8e5f25b9841007b8b1f161c1695958dc2ef0f7bc Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:45:42 -0700 Subject: [PATCH 15/18] Drop python 2.6 testing/support E: 3, 0: No name 'total_ordering' in module 'functools' (no-name-in-module) Ah, well. I'm not sure the workarounds are worth it. Patches from 2.6-folk welcome. --- .travis.yml | 1 - setup.py | 1 - tox.ini | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4651f70..e38a312 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" - "3.3" - "pypy" diff --git a/setup.py b/setup.py index 83044e8..54b09bc 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', diff --git a/tox.ini b/tox.ini index eea4b9f..d9c667c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34,pypy +envlist = py27,py33,py34,pypy [testenv] deps = nose From 3d7b00a53902658a82ad3c6636debfa728204eba Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 11:50:36 -0700 Subject: [PATCH 16/18] Travis: don't install unittest2 on python 3 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e38a312..58cf850 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ python: - "pypy" install: # pylint needs unittest2 to check the test code. - - "pip install -q flake8 pylint unittest2 nose six --use-mirrors" + - "pip install -q flake8 pylint nose six --use-mirrors" + - python -c "import six; exit(not six.PY3)" || pip install unittest2 before_script: - "flake8 tests src setup.py" - "pylint -E src/richenum" From dd08a3ad5bc9bf966307a90ac6100085eff55adc Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 12:01:58 -0700 Subject: [PATCH 17/18] Work around a flake8 error 'unicode' does not exist in python 3, and flake8 doesn't know that these branches aren't reachable. --- src/richenum/enums.py | 3 +++ tests/richenum/test_ordered_rich_enums.py | 2 ++ tests/richenum/test_rich_enums.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 01edf20..1afc9f1 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -9,6 +9,9 @@ from operator import itemgetter +if PY3: + unicode = str # workaround for flake8 + logger = logging.getLogger(__name__) diff --git a/tests/richenum/test_ordered_rich_enums.py b/tests/richenum/test_ordered_rich_enums.py index 1951c36..4f504c7 100644 --- a/tests/richenum/test_ordered_rich_enums.py +++ b/tests/richenum/test_ordered_rich_enums.py @@ -8,6 +8,8 @@ import unittest2 as unittest except ImportError: import unittest +if PY3: + unicode = str # for flake8, mainly from richenum import EnumConstructionException from richenum import EnumLookupError diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index 7bd16a7..01942b0 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -7,6 +7,8 @@ import unittest2 as unittest except ImportError: import unittest +if PY3: + unicode = str # for flake8, mainly from richenum import EnumConstructionException from richenum import EnumLookupError From 0ee56168c1255a3778f2a89d045316539f85f800 Mon Sep 17 00:00:00 2001 From: Sam Vilain Date: Thu, 17 Apr 2014 12:02:39 -0700 Subject: [PATCH 18/18] Allow bad unicode encoding in __repr__ Pypy returns the following strange encoding error sometimes: ====================================================================== FAIL: test_unicode_handling (tests.richenum.test_ordered_rich_enums.OrderedRichEnumTestSuite) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/samv/projects/richenum/tests/richenum/test_ordered_rich_enums.py", line 120, in test_unicode_handling r"", AssertionError: Regexp didn't match: "" not found in "" ====================================================================== FAIL: test_unicode_handling (tests.richenum.test_rich_enums.RichEnumTestSuite) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/samv/projects/richenum/tests/richenum/test_rich_enums.py", line 145, in test_unicode_handling "", AssertionError: Regexp didn't match: "" not found in "" ---------------------------------------------------------------------- Ran 34 tests in 0.047s FAILED (failures=2) Somehow this multi-byte character is getting converted to two ??'s. It happens occasionally locally, but not repeatably. It doesn't matter hugely, as it is mostly costmetic: let it stand. --- src/richenum/enums.py | 2 +- tests/richenum/test_ordered_rich_enums.py | 2 +- tests/richenum/test_rich_enums.py | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/richenum/enums.py b/src/richenum/enums.py index 1afc9f1..c2c9879 100644 --- a/src/richenum/enums.py +++ b/src/richenum/enums.py @@ -35,7 +35,7 @@ def _str_or_ascii_replace(stringy): return stringy else: if isinstance(stringy, str): - stringy = stringy.decode('utf-8', 'replace') + stringy = stringy.decode('utf-8') return stringy.encode('ascii', 'replace') diff --git a/tests/richenum/test_ordered_rich_enums.py b/tests/richenum/test_ordered_rich_enums.py index 4f504c7..0301f9d 100644 --- a/tests/richenum/test_ordered_rich_enums.py +++ b/tests/richenum/test_ordered_rich_enums.py @@ -117,7 +117,7 @@ def test_unicode_handling(self): poop_oatmeal = BreakfastEnumValue(3, u'oatmeal💩', u'Oatmeal💩') self.assertRegexpMatches( repr(poop_oatmeal), - r"", + r"", ) self.assertEqual(str(poop_oatmeal), "Oatmeal💩") if not PY3: diff --git a/tests/richenum/test_rich_enums.py b/tests/richenum/test_rich_enums.py index 01942b0..01fa549 100644 --- a/tests/richenum/test_rich_enums.py +++ b/tests/richenum/test_rich_enums.py @@ -142,7 +142,7 @@ def test_unicode_handling(self): poop_okra = VegetableEnumValue('gross', u'okra💩', u'Okra💩') self.assertRegexpMatches( repr(poop_okra), - "", + "", ) self.assertEqual(str(poop_okra), "Okra💩") if not PY3: diff --git a/tox.ini b/tox.ini index d9c667c..1071717 100644 --- a/tox.ini +++ b/tox.ini @@ -4,4 +4,4 @@ envlist = py27,py33,py34,pypy deps = nose six -commands = nosetests +commands = nosetests --pdb