Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python3 etc #31

Merged
merged 18 commits into from
Jun 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "pypy"
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 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"
- "pylint -E tests/richenum"
- "pylint -E setup.py"
script: "python setup.py test"
script: "nosetests -s -v"
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Contributors
| `Akshay Shah <http://github.com/akshayjshah>`_
| `Dale Hui <http://github.com/dhui>`_
| `Robert MacCloy <http://github.com/rbm>`_
| `Sam Vilain <http://github.com/samv>`_
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ django-richenum

| `PyPi <https://pypi.python.org/pypi/django-richenum/>`_

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
Expand Down
24 changes: 20 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -16,7 +19,19 @@
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.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',
Expand All @@ -25,5 +40,6 @@
package_dir={'': 'src'},
packages=find_packages('src'),
tests_require=tests_require,
install_requires=['six'],
test_suite='tests'
)
107 changes: 72 additions & 35 deletions src/richenum/enums.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import collections
import copy
from functools import total_ordering
import logging
import new
import numbers
from six import PY3
from six import string_types
from six import with_metaclass

from operator import itemgetter

if PY3:
unicode = str # workaround for flake8


logger = logging.getLogger(__name__)

Expand All @@ -23,6 +30,29 @@ class EnumLookupError(Exception):
pass


def _str_or_ascii_replace(stringy):
if PY3:
return stringy
else:
if isinstance(stringy, str):
stringy = stringy.decode('utf-8')
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.
Expand All @@ -35,21 +65,25 @@ 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.
# 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(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 e._dict.items()]).get
e = type('Enum', (_EnumMethods,), dict((k, v) for k, v in _items(en)))

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

return e


@total_ordering
class RichEnumValue(object):
def __init__(self, canonical_name, display_name, *args, **kwargs):
self.canonical_name = canonical_name
Expand All @@ -58,24 +92,26 @@ 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)

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:
Expand All @@ -97,10 +133,11 @@ 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)
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")
Expand All @@ -111,22 +148,21 @@ 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:
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):
Expand Down Expand Up @@ -156,7 +192,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")

Expand Down Expand Up @@ -228,8 +266,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
):
Expand All @@ -240,11 +277,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'):
Expand All @@ -261,7 +298,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).
Expand All @@ -286,10 +323,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),
Expand All @@ -311,8 +348,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
31 changes: 22 additions & 9 deletions tests/richenum/test_ordered_rich_enums.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# -*- coding: utf-8 -*-

# pylint: disable=E1101

import copy
import unittest2 as unittest
from six import PY3
try:
import unittest2 as unittest
except ImportError:
import unittest
if PY3:
unicode = str # for flake8, mainly

from richenum import EnumConstructionException
from richenum import EnumLookupError
Expand Down Expand Up @@ -32,16 +41,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'):
Expand Down Expand Up @@ -105,7 +114,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), "<BreakfastEnumValue #3: oatmeal? ('Oatmeal?')>")
poop_oatmeal = BreakfastEnumValue(3, u'oatmeal💩', u'Oatmeal💩')
self.assertRegexpMatches(
repr(poop_oatmeal),
r"<BreakfastEnumValue #3: oatmeal..? \('Oatmeal..?'\)>",
)
self.assertEqual(str(poop_oatmeal), "Oatmeal💩")
self.assertEqual(unicode(poop_oatmeal), u"Oatmeal💩")
if not PY3:
self.assertEqual(unicode(poop_oatmeal), u"Oatmeal💩")
Loading