add Python 2 suppord for settings, init, and start
diff --git a/aenum/__init__.py b/aenum/__init__.py
index 3d7e0c1..e5a955f 100644
--- a/aenum/__init__.py
+++ b/aenum/__init__.py
@@ -11,7 +11,7 @@
'NamedTuple',
]
-version = 1, 4, 3, 2
+version = 1, 4, 3, 3
pyver = float('%s.%s' % _sys.version_info[:2])
@@ -254,6 +254,29 @@
# defined now for immediate use
+def enumsort(things):
+ """
+ sorts things by value if all same type; otherwise by name
+ """
+ if not things:
+ return things
+ sort_type = type(things[0])
+ if not issubclass(sort_type, tuple):
+ # direct sort or type error
+ if not all((type(v) is sort_type) for v in things[1:]):
+ raise TypeError('Cannot sort items of different types')
+ return sorted(things)
+ else:
+ # expecting list of (name, value) tuples
+ sort_type = type(things[0][1])
+ try:
+ if all((type(v[1]) is sort_type) for v in things[1:]):
+ return sorted(things, key=lambda i: i[1])
+ else:
+ raise TypeError('try name sort instead')
+ except TypeError:
+ return sorted(things, key=lambda i: i[0])
+
def export(collection, namespace=None):
"""
export([collection,] namespace) -> Export members to target namespace.
@@ -309,7 +332,7 @@
args = ', '.join(['%r' % a for a in self.args])
if args:
final.append(args)
- kwds = ', '.join([('%s=%r') % (k, v) for k, v in sorted(self.kwds.items())])
+ kwds = ', '.join([('%s=%r') % (k, v) for k, v in enumsort(list(self.kwds.items()))])
if kwds:
final.append(kwds)
return 'enum(%s)' % ', '.join(final)
@@ -322,11 +345,18 @@
"""
def __init__(self, locked=True, start=1, multivalue=False):
super(_EnumDict, self).__init__()
+ # list of enum members
self._member_names = []
+ # starting value for AutoNumber
self._value = start - 1
+ # when the magic turns off
self._locked = locked
+ # if multiple values are allowed
self._multivalue = multivalue
+ # list of temporary names
self._ignore = []
+ # if _sunder_ values can be changed via the class body
+ self._init = True
def __getitem__(self, key):
if self._locked or _is_dunder(key) or key in self:
@@ -341,7 +371,7 @@
raise KeyError('%s not found' % key)
def __setitem__(self, key, value):
- """Changes anything not dundered or not a descriptor.
+ """Changes anything not sundured, dundered, nor a descriptor.
If an enum member name is used twice, an error is raised; duplicate
values are not checked for.
@@ -349,11 +379,35 @@
Single underscore (sunder) names are reserved.
"""
if _is_sunder(key):
- if key == '_ignore_':
+ if key not in ('_init_', '_settings_', '_order_', '_ignore_', '_start_'):
+ raise ValueError('_names_ are reserved for future Enum use')
+ elif not self._init:
+ raise ValueError('cannot set %r after init phase' % key)
+ elif key == '_ignore_':
value = value.split()
self._ignore = value
- elif key not in ('_init_', '_settings_', '_order_'):
- raise ValueError('_names_ are reserved for future Enum use')
+ already = set(value) & set(self._member_names)
+ if already:
+ raise ValueError('_ignore_ cannot specify already set names: %r' % (already, ))
+ elif key == '_start_':
+ self._value = value - 1
+ self._locked = False
+ elif key == '_settings_':
+ if not isinstance(value, tuple):
+ value = value,
+ if NoAlias in value and Unique in value:
+ raise TypeError('cannot specify both NoAlias and Unique')
+ # elif MultiValue in value and self._init is not None:
+ # raise TypeError('cannot specify both MultiValue and _init_ fields')
+ elif MultiValue in value and NoAlias in value:
+ raise TypeError('cannot specify both MultiValue and NoAlias')
+ allowed_settings = dict.fromkeys(['autonumber', 'noalias', 'unique', 'multivalue'])
+ for arg in value:
+ if arg not in allowed_settings:
+ raise TypeError('unknown qualifier: %r' % arg)
+ allowed_settings[arg] = True
+ self._locked = not allowed_settings['autonumber']
+ self._multivalue = allowed_settings['multivalue']
elif _is_dunder(key):
if key == '__order__':
key = '_order_'
@@ -365,17 +419,30 @@
elif key in self._ignore:
pass
elif not _is_descriptor(value):
+ self._init = False
if key in self:
# enum overwriting a descriptor?
raise TypeError('%s already defined as: %r' % (key, self[key]))
self._member_names.append(key)
if self._multivalue and isinstance(value, tuple):
self._value = value[0]
- else:
- self._value = value
+ elif not self._locked:
+ if isinstance(value, int):
+ self._value = value
+ elif isinstance(value, tuple):
+ if not value or not isinstance(value[0], int):
+ value = self._value + 1
+ self._value = value
+ else:
+ self._value = value[0]
+ else:
+ count = self._value + 1
+ self._value = count
+ value = count, value
else:
# not a new member, turn off the autoassign magic
self._locked = True
+ self._init = False
super(_EnumDict, self).__setitem__(key, value)
@@ -412,15 +479,33 @@
super(EnumMeta, cls).__init__(*args)
def __new__(metacls, cls, bases, clsdict, init='', start=None, settings=()):
+ member_type, first_enum = metacls._get_mixins_(bases)
+ # inherit previous flags
+ autonumber = multivalue = noalias = unique = False
+ if first_enum is not None:
+ autonumber = first_enum._auto_number_
+ multivalue = first_enum._multi_value_
+ noalias = first_enum._no_alias_
+ unique = first_enum._unique_
# check for custom settings
if not isinstance(settings, tuple):
- settings = settings,
- allowed_settings = dict.fromkeys(['autonumber', 'noalias', 'unique', 'multivalue'])
- cls_init = clsdict.get('_init_')
+ settings = (settings, )
+ allowed_settings = dict(
+ autonumber=autonumber,
+ noalias=noalias,
+ unique=unique,
+ multivalue=multivalue,
+ )
+ cls_init = clsdict.pop('_init_', None)
if cls_init and init:
raise TypeError('init specified in constructor and in class body')
init = cls_init or init
- cls_settings = clsdict.get('_settings_')
+ cls_start = clsdict.pop('_start_', None)
+ if cls_start is not None and start is not None:
+ raise TypeError('start specified in constructor and in class body')
+ if cls_start is not None:
+ start = cls_start
+ cls_settings = clsdict.pop('_settings_', None)
if cls_settings and settings:
raise TypeError('settings specified in constructor and in class body')
else:
@@ -430,20 +515,65 @@
for arg in settings:
if arg not in allowed_settings:
raise TypeError('unknown qualifier: %r' % arg)
- allowed_settings[arg] = True
- autonumber = allowed_settings['autonumber']
+ allowed_settings[arg] = allowed_settings[arg] or True
+ autonumber = allowed_settings['autonumber'] or (start is not None)
multivalue = allowed_settings['multivalue']
noalias = allowed_settings['noalias']
unique = allowed_settings['unique']
+ cls_settings = []
+ if autonumber:
+ cls_settings.append(AutoNumber)
+ if multivalue:
+ cls_settings.append(MultiValue)
+ if noalias:
+ cls_settings.append(NoAlias)
+ if unique:
+ cls_settings.append(Unique)
+ cls_settings = tuple(cls_settings)
+ # at this point allowed_settings, cls_settings, and the same-named variables, are current
+ # with both inherited settings and directly specified settings
+ if autonumber and init and not (init == 'value' or init.startswith('value ')):
+ init = 'value, ' + init
+ if start is None:
+ start = 1
# an Enum class is final once enumeration items have been defined; it
# cannot be mixed with other types (int, float, etc.) if it has an
# inherited __new__ unless a new __new__ is defined (or the resulting
# class will fail).
if type(clsdict) is dict:
+ # py2 support for _sunder_ configuration
+ _order_ = clsdict.get('_order_')
+ if _order_ is not None:
+ del clsdict['_order_']
+ else:
+ _order_ = clsdict.get('__order__')
+ if _order_ is not None:
+ del clsdict['__order__']
+ members = dict([
+ (k, v) for (k, v) in clsdict.items()
+ if not (_is_sunder(k) or _is_dunder(k) or _is_descriptor(k))
+ ])
+ if _order_ is None:
+ _order_ = [name for (name, value) in enumsort(list(members.items()))]
+ else:
+ _order_ = _order_.replace(',', ' ').split()
+ aliases = [name for name in members if name not in _order_]
+ if noalias and aliases:
+ raise ValueError('all members must be in _order_ if specified and using NoAlias')
+ _order_ += aliases
original_dict = clsdict
- clsdict = _EnumDict()
- for k, v in original_dict.items():
+ clsdict = _EnumDict(locked=not autonumber, start=start, multivalue=multivalue)
+ # add sunders first
+ clsdict['_init_'] = cls_init
+ clsdict['_settings_'] = cls_settings
+ clsdict['_ignore_'] = original_dict.pop('_ignore_', '')
+ for k in _order_:
+ v = original_dict[k]
clsdict[k] = v
+ for k, v in original_dict.items():
+ if k not in _order_:
+ clsdict[k] = v
+ del _order_
else:
clsdict._locked = True
@@ -457,18 +587,11 @@
if init and clsdict.get('__init__'):
raise TypeError('cannot specify init and define an __init__ method')
- member_type, first_enum = metacls._get_mixins_(bases)
__new__, save_new, use_args = metacls._find_new_(
clsdict,
member_type,
first_enum,
)
- if first_enum is not None:
- # inherit previous flags
- autonumber = autonumber or first_enum._auto_number_
- multivalue = multivalue or first_enum._multi_value_
- noalias = noalias or first_enum._no_alias_
- unique = unique or first_enum._unique_
# save enum items into separate mapping so they don't get baked into
# the new class
members = dict((k, clsdict[k]) for k in clsdict._member_names)
@@ -482,27 +605,19 @@
elif isinstance(obj, _RouteClassAttributeToGetattr):
obj.name = name
- # py2 support for definition order
+ # py3 support for definition order
_order_ = clsdict.get('_order_')
if _order_ is None:
- if pyver < 3.0:
- try:
- _order_ = [name for (name, value) in sorted(members.items(), key=lambda item: item[1])]
- except TypeError:
- _order_ = [name for name in sorted(members.keys())]
- else:
- _order_ = clsdict._member_names
-
+ _order_ = clsdict._member_names
else:
del clsdict['_order_']
_order_ = _order_.replace(',', ' ').split()
aliases = [name for name in members if name not in _order_]
if noalias and aliases:
raise ValueError('all members must be in _order_ if specified and using NoAlias')
- if pyver >= 3.0:
- unique_members = [n for n in clsdict._member_names if n in _order_]
- if _order_ != unique_members:
- raise TypeError('member order does not match _order_')
+ unique_members = [n for n in clsdict._member_names if n in _order_]
+ if _order_ != unique_members:
+ raise TypeError('member order does not match _order_')
_order_ += aliases
# check for illegal enum names (any others?)
@@ -557,9 +672,11 @@
value = kwds.pop('value')
else:
value, args = args[0], args[1:]
- args, more_args = (), args
+ # args, more_args = (), args
+ args, more_args = (value, ), args
elif multivalue:
- value, more_values, args = args[0], args[1:], ()
+ args, more_values = args[0:1], args[1:]
+ value = args[0]
if member_type is tuple: # special case for tuple enums
args = (args, ) # wrap it one more time
if not use_args or not (args or kwds):
@@ -573,7 +690,7 @@
value = enum_member._value_
enum_member._name_ = member_name
enum_member.__objclass__ = enum_class
- enum_member.__init__(*(args or more_args), **kwds)
+ enum_member.__init__(*(more_args or args), **kwds)
# If another member with the same value was already defined, the
# new member becomes an alias to the existing one.
if noalias:
@@ -810,7 +927,7 @@
bases = (cls, )
else:
bases = (type, cls)
- clsdict = metacls.__prepare__(class_name, bases)
+ clsdict = {}
_order_ = []
# special processing needed for names?
@@ -1232,26 +1349,21 @@
Automatically assign increasing values to members.
Py3: numbers match creation order
- Py2: numbers are randomly assigned
+ Py2: numbers are assigned alphabetically by member name
"""
- def __new__(cls):
- value = len(cls.__members__) + 1
- obj = object.__new__(cls)
- obj._value_ = value
- return obj
+ _settings_ = AutoNumber
-class UniqueEnum(Enum):
+class MultiValueEnum(Enum):
"""
- Ensure no duplicate values exist.
+ Multiple values can map to each member.
"""
- def __init__(self, *args):
- cls = self.__class__
- if any(self.value == e.value for e in cls):
- a = self.name
- e = cls(self.value).name
- raise ValueError(
- "aliases not allowed in UniqueEnum: %r --> %r"
- % (a, e))
+ _settings_ = MultiValue
+
+class NoAliasEnum(Enum):
+ """
+ Duplicate value members are distinct, and cannot be looked up by value.
+ """
+ _settings_ = NoAlias
class OrderedEnum(Enum):
"""
@@ -1282,6 +1394,12 @@
if protocol is sqlite3.PrepareProtocol:
return self.name
+class UniqueEnum(Enum):
+ """
+ Ensure no duplicate values exist.
+ """
+ _settings_ = Unique
+
def convert(enum, name, module, filter, source=None):
"""
diff --git a/aenum/test.py b/aenum/test.py
index 982fda6..fb8b1f4 100644
--- a/aenum/test.py
+++ b/aenum/test.py
@@ -3,7 +3,7 @@
import sys
import unittest
from aenum import Enum, IntEnum, AutoNumberEnum, OrderedEnum, UniqueEnum, unique, skip, extend_enum
-from aenum import EnumMeta, NamedTuple, TupleSize, NamedConstant, constant, NoAlias
+from aenum import EnumMeta, NamedTuple, TupleSize, NamedConstant, constant, NoAlias, AutoNumber, Unique, AutoNumber
from collections import OrderedDict
from datetime import timedelta
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
@@ -1722,9 +1722,9 @@
unprocessed = (1, "Unprocessed")
payment_complete = (2, "Payment Complete")
- self.assertEqual(list(LabelledList), [LabelledList.unprocessed, LabelledList.payment_complete])
self.assertEqual(LabelledList.unprocessed, 1)
self.assertEqual(LabelledList(1), LabelledList.unprocessed)
+ self.assertEqual(list(LabelledList), [LabelledList.unprocessed, LabelledList.payment_complete])
def test_empty_with_functional_api(self):
empty = aenum.IntEnum('Foo', {})
@@ -1769,6 +1769,59 @@
rojo = 1
self.assertFalse(Settings.red is Settings.rojo)
+ def test_auto_and_init(self):
+ class Field(IntEnum):
+ _order_ = 'TYPE START'
+ _settings_ = AutoNumber
+ _init_ = '__doc__'
+ TYPE = "Char, Date, Logical, etc."
+ START = "Field offset in record"
+ self.assertEqual(Field.TYPE, 1)
+ self.assertEqual(Field.START, 2)
+ self.assertEqual(Field.TYPE.__doc__, 'Char, Date, Logical, etc.')
+ self.assertEqual(Field.START.__doc__, 'Field offset in record')
+
+ def test_auto_and_start(self):
+ class Field(IntEnum):
+ _order_ = 'TYPE START'
+ _start_ = 0
+ _init_ = '__doc__'
+ TYPE = "Char, Date, Logical, etc."
+ START = "Field offset in record"
+ self.assertEqual(Field.TYPE, 0)
+ self.assertEqual(Field.START, 1)
+ self.assertEqual(Field.TYPE.__doc__, 'Char, Date, Logical, etc.')
+ self.assertEqual(Field.START.__doc__, 'Field offset in record')
+
+ def test_auto_and_init_and_some_values(self):
+ class Field(IntEnum):
+ _order_ = 'TYPE START'
+ _settings_ = AutoNumber
+ _init_ = '__doc__'
+ TYPE = "Char, Date, Logical, etc."
+ START = "Field offset in record"
+ BLAH = 5, "test blah"
+ BELCH = 'test belch'
+ self.assertEqual(Field.TYPE, 1)
+ self.assertEqual(Field.START, 2)
+ self.assertEqual(Field.BLAH, 5)
+ self.assertEqual(Field.BELCH, 6)
+ self.assertEqual(Field.TYPE.__doc__, 'Char, Date, Logical, etc.')
+ self.assertEqual(Field.START.__doc__, 'Field offset in record')
+ self.assertEqual(Field.BLAH.__doc__, 'test blah')
+ self.assertEqual(Field.BELCH.__doc__, 'test belch')
+
+ def test_combine_new_settings_with_old_settings(self):
+ class Auto(Enum):
+ _settings_ = Unique
+ with self.assertRaises(ValueError):
+ class AutoUnique(Auto):
+ _settings_ = AutoNumber
+ BLAH = ()
+ BLUH = ()
+ ICK = 1
+ print(list(AutoUnique))
+
def test_timedelta(self):
class Period(timedelta, Enum):
'''
diff --git a/setup.py b/setup.py
index d6fc7ef..566cff0 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@
data = dict(
name='aenum',
- version='1.4.3a2',
+ version='1.4.3a3',
url='https://bitbucket.org/stoneleaf/aenum',
packages=['aenum'],
package_data={