diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -30,55 +30,120 @@ one decorator, :func:`unique`. .. class:: Enum Base class for creating enumerated constants. See section `Functional API`_ for an alternate construction syntax. .. class:: IntEnum Base class for creating enumerated constants that are also subclasses of :class:`int`. +.. class:: AutoEnum + + Base class for creating automatically numbered members (may + be combined with IntEnum if desired). + + .. versionadded:: 3.6 + +.. class:: AutoNameEnum + + Base class for creating members whose values are lower-cased + versions of the name. + + .. versionadded:: 3.6 + +.. class:: UniqueEnum + + Base class for Enums with no aliases. + + .. versionadded:: 3.6 + .. function:: unique Enum class decorator that ensures only one name is bound to any one value. +.. attribute:: AutoNumber + + Flag that can be used to create an AutoEnum-like Enum. + +.. attribute:: AutoName + + Flag that can be used to create an AutoNameEnum-like Enum. + +.. attribute:: Unique + + Flag that can be used to create a Unique-like Enum. + Creating an Enum ---------------- Enumerations are created using the :keyword:`class` syntax, which makes them easy to read and write. An alternative creation method is described in -`Functional API`_. To define an enumeration, subclass :class:`Enum` as -follows:: +`Functional API`_. To define a simple enumeration, subclass :class:`AutoEnum` +as follows:: - >>> from enum import Enum - >>> class Color(Enum): - ... red = 1 - ... green = 2 - ... blue = 3 + >>> from enum import AutoEnum + >>> class Color(AutoEnum): + ... red + ... green + ... blue ... .. note:: Nomenclature - The class :class:`Color` is an *enumeration* (or *enum*) - The attributes :attr:`Color.red`, :attr:`Color.green`, etc., are *enumeration members* (or *enum members*). - The enum members have *names* and *values* (the name of :attr:`Color.red` is ``red``, the value of :attr:`Color.blue` is ``3``, etc.) .. note:: Even though we use the :keyword:`class` syntax to create Enums, Enums are not normal Python classes. See `How are Enums different?`_ for more details. +If numbers for the values is not desired, but lower-cased versions of the names +are, simply change the base class to :class:`AutoNameEnum`:: + + >>> from enum import AutoNameEnum + >>> class Color(AutoNameEnum): + ... red + ... green + ... blue + ... + +Now the value of :attr:`Color.green`, for example, is ``green``. + +Finally, if full control of the values is needed, use :class:`Enum` as the base +class and specify the values manually:: + + >>> from enum import Enum + >>> class Color(Enum): + ... red = 19 + ... green = 7.9182 + ... blue = 'periwinkle' + ... + +We'll use a simple Enum for the examples below:: + + >>> class Color(Enum): + ... red = 1 + ... green = 2 + ... blue = 3 + ... + +Enum Details +------------ + Enumeration members have human readable string representations:: >>> print(Color.red) Color.red ...while their ``repr`` has more information:: >>> print(repr(Color.red)) @@ -183,22 +248,42 @@ return A:: Attempting to create a member with the same name as an already defined attribute (another member, a method, etc.) or attempting to create an attribute with the same name as a member is not allowed. Ensuring unique enumeration values ---------------------------------- By default, enumerations allow multiple names as aliases for the same value. -When this behavior isn't desired, the following decorator can be used to -ensure each value is used only once in the enumeration: +When this behavior isn't desired, either subclass :class:`UniqueEnum` or use +the :attr:`unique` decorator to ensure each value is used only once in the +enumeration: + +.. class:: UniqueEnum + +A base class to ensure no duplicate values / aliases exist in the created +enumeration. If any exist a :exc:`ValueError` is raised with the details:: + + >>> from enum import UniqueEnum + >>> class Mistake(UniqueEnum): + ... one = 1 + ... two = 2 + ... three = 3 + ... four = 3 + ... + Traceback (most recent call last): + ... + ValueError: duplicate values found in Mistake: + members 'three', 'four' all have the value of 3 + + .. versionadded:: 3.6 .. decorator:: unique A :keyword:`class` decorator specifically for enumerations. It searches an enumeration's :attr:`__members__` gathering any aliases it finds; if any are found :exc:`ValueError` is raised with the details:: >>> from enum import Enum, unique >>> @unique ... class Mistake(Enum): @@ -250,21 +335,21 @@ Enumeration members are compared by iden False >>> Color.red is not Color.blue True Ordered comparisons between enumeration values are *not* supported. Enum members are not integers (but see `IntEnum`_ below):: >>> Color.red < Color.blue Traceback (most recent call last): File "", line 1, in - TypeError: unorderable types: Color() < Color() + TypeError: '<' not supported between instances of 'Color' and 'Color' Equality comparisons are defined though:: >>> Color.blue == Color.red False >>> Color.blue != Color.red True >>> Color.blue == Color.blue True @@ -273,24 +358,24 @@ Comparisons against non-enumeration valu below):: >>> Color.blue == 2 False Allowed members and attributes of enumerations ---------------------------------------------- The examples above use integers for enumeration values. Using integers is -short and handy (and provided by default by the `Functional API`_), but not -strictly enforced. In the vast majority of use-cases, one doesn't care what -the actual value of an enumeration is. But if the value *is* important, -enumerations can have arbitrary values. +short and handy (and provided by default by :class:`AutoEnum` and the +`Functional API`_), but not strictly enforced. In the vast majority of +use-cases, one doesn't care what the actual value of an enumeration is. +But if the value *is* important, enumerations can have arbitrary values. Enumerations are Python classes, and can have methods and special methods as usual. If we have this enumeration:: >>> class Mood(Enum): ... funky = 1 ... happy = 3 ... ... def describe(self): ... # self is the member here @@ -386,47 +471,51 @@ The :class:`Enum` class is callable, pro >>> Animal = Enum('Animal', 'ant bee cat dog') >>> Animal >>> Animal.ant >>> Animal.ant.value 1 >>> list(Animal) [, , , ] -The semantics of this API resemble :class:`~collections.namedtuple`. The first -argument of the call to :class:`Enum` is the name of the enumeration. +The semantics of this API resemble :class:`~collections.namedtuple`. -The second argument is the *source* of enumeration member names. It can be a -whitespace-separated string of names, a sequence of names, a sequence of -2-tuples with key/value pairs, or a mapping (e.g. dictionary) of names to -values. The last two options enable assigning arbitrary values to -enumerations; the others auto-assign increasing integers starting with 1 (use -the ``start`` parameter to specify a different starting value). A -new class derived from :class:`Enum` is returned. In other words, the above -assignment to :class:`Animal` is equivalent to:: +- the first argument of the call to :class:`Enum` is the name of the + enumeration; + +- the second argument is the *source* of enumeration member names. It can be a + whitespace-separated string of names, a sequence of names, a sequence of + 2-tuples with key/value pairs, or a mapping (e.g. dictionary) of names to + values; + +- the last two options enable assigning arbitrary values to enumerations; the + others auto-assign increasing integers starting with 1 (use the ``start`` + parameter to specify a different starting value). A new class derived from + :class:`Enum` is returned. In other words, the above assignment to + :class:`Animal` is equivalent to:: >>> class Animal(Enum): ... ant = 1 ... bee = 2 ... cat = 3 ... dog = 4 ... The reason for defaulting to ``1`` as the starting number and not ``0`` is that ``0`` is ``False`` in a boolean sense, but enum members all evaluate to ``True``. Pickling enums created with the functional API can be tricky as frame stack implementation details are used to try and figure out which module the enumeration is being created in (e.g. it will fail if you use a utility -function in separate module, and also may not work on IronPython or Jython). +function in a separate module, and also may not work on IronPython or Jython). The solution is to specify the module name explicitly as follows:: >>> Animal = Enum('Animal', 'ant bee cat dog', module=__name__) .. warning:: If ``module`` is not supplied, and Enum cannot determine what it is, the new Enum members will not be unpicklable; to keep errors closer to the source, pickling will be disabled. @@ -468,24 +557,91 @@ The complete signature is:: :start: number to start counting at if only names are passed in. .. versionchanged:: 3.5 The *start* parameter was added. Derived Enumerations -------------------- +AutoEnum +^^^^^^^^ + +This version of :class:`Enum` automatically assigns numbers as the values +for the enumeration members, while still allowing values to be specified +when needed:: + + >>> from enum import AutoEnum + >>> class Color(AutoEnum): + ... red + ... green = 5 + ... blue + ... + >>> list(Color) + [, , ] + +.. note:: Name Lookup + + By default the names :func:`property`, :func:`classmethod`, and + :func:`staticmethod` are shielded from becoming members. To enable + them, or to specify a different set of shielded names, specify the + ignore list:: + + >>> class AddressType(AutoEnum, ignore='classmethod staticmethod'): + ... pobox + ... mailbox + ... property + ... + +.. versionadded:: 3.6 + +AutoNameEnum +^^^^^^^^^^^^ + +This version of :class:`Enum` automatically assigns the lower-cased version +of each name as the value for the enumeration members:: + + >>> from enum import AutoNameEnum + >>> class Color(AutoNameEnum): + ... red + ... green + ... blue + ... + >>> list(Color) + [, , ] + +.. versionadded:: 3.6 + +UniqueEnum +^^^^^^^^^^ + +This version of :class:`Enum` automatically does not allow duplicate values:: + + >>> from enum import UniqueEnum + >>> class Mistake(UniqueEnum): + ... one = 1 + ... two = 2 + ... three = 3 + ... four = 3 + ... + Traceback (most recent call last): + ... + ValueError: duplicate values found in Mistake: + members 'three', 'four' all have the value of 3 + +.. versionadded:: 3.6 + IntEnum ^^^^^^^ -A variation of :class:`Enum` is provided which is also a subclass of +Another variation of :class:`Enum` which is also a subclass of :class:`int`. Members of an :class:`IntEnum` can be compared to integers; by extension, integer enumerations of different types can also be compared to each other:: >>> from enum import IntEnum >>> class Shape(IntEnum): ... circle = 1 ... square = 2 ... >>> class Request(IntEnum): @@ -514,40 +670,41 @@ However, they still can't be compared to :class:`IntEnum` values behave like integers in other ways you'd expect:: >>> int(Shape.circle) 1 >>> ['a', 'b', 'c'][Shape.circle] 'b' >>> [i for i in range(Shape.square)] [0, 1] -For the vast majority of code, :class:`Enum` is strongly recommended, -since :class:`IntEnum` breaks some semantic promises of an enumeration (by -being comparable to integers, and thus by transitivity to other +For the vast majority of code, :class:`Enum` and :class:`AutoEnum` are strongly +recommended, since :class:`IntEnum` breaks some semantic promises of an +enumeration (by being comparable to integers, and thus by transitivity to other unrelated enumerations). It should be used only in special cases where -there's no other choice; for example, when integer constants are -replaced with enumerations and backwards compatibility is required with code -that still expects integers. - +there's no other choice; for example, when integer constants are replaced with +enumerations and backwards compatibility is required with code that still +expects integers. Others ^^^^^^ While :class:`IntEnum` is part of the :mod:`enum` module, it would be very simple to implement independently:: class IntEnum(int, Enum): pass This demonstrates how similar derived enumerations can be defined; for example -a :class:`StrEnum` that mixes in :class:`str` instead of :class:`int`. +an :class:`AutoStrEnum` that mixes in :class:`str` with :class:`AutoNameEnum` +to get members that are :class:`str` (but keep in mind the warnings for +:class:`IntEnum`). Some rules: 1. When subclassing :class:`Enum`, mix-in types must appear before :class:`Enum` itself in the sequence of bases, as in the :class:`IntEnum` example above. 2. While :class:`Enum` can have members of any type, once you mix in an additional type, all the members must have values of that type, e.g. :class:`int` above. This restriction does not apply to mix-ins which only add methods and don't specify another data type such as :class:`int` or @@ -560,45 +717,48 @@ 4. %-style formatting: `%s` and `%r` ca `%i` or `%h` for IntEnum) treat the enum member as its mixed-in type. 5. :ref:`Formatted string literals `, :meth:`str.format`, and :func:`format` will use the mixed-in type's :meth:`__format__`. If the :class:`Enum` class's :func:`str` or :func:`repr` is desired, use the `!s` or `!r` format codes. Interesting examples -------------------- -While :class:`Enum` and :class:`IntEnum` are expected to cover the majority of -use-cases, they cannot cover them all. Here are recipes for some different -types of enumerations that can be used directly, or as examples for creating -one's own. +While :class:`Enum`, :class:`AutoEnum`, and :class:`IntEnum` are expected +to cover the majority of use-cases, they cannot cover them all. Here are +recipes for some different types of enumerations that can be used directly, +or as examples for creating one's own. -AutoNumber -^^^^^^^^^^ +AutoDocEnum +^^^^^^^^^^^ Avoids having to specify the value for each enumeration member:: - >>> class AutoNumber(Enum): - ... def __new__(cls): + >>> class AutoDocEnum(Enum): + ... def __new__(cls, doc): ... value = len(cls.__members__) + 1 ... obj = object.__new__(cls) ... obj._value_ = value + ... obj.__doc__ = doc ... return obj ... - >>> class Color(AutoNumber): - ... red = () - ... green = () - ... blue = () + >>> class Color(AutoDocEnum): + ... red = 'stop' + ... green = 'go' + ... blue = 'what?' ... >>> Color.green.value == 2 True + >>> Color.green.__doc__ + 'go' .. note:: The :meth:`__new__` method, if defined, is used during creation of the Enum members; it is then replaced by Enum's :meth:`__new__` which is used after class creation for lookup of existing members. OrderedEnum ^^^^^^^^^^^ @@ -727,21 +887,21 @@ class itself, and then puts a custom :me that no new ones are ever instantiated by returning only the existing member instances. Finer Points ^^^^^^^^^^^^ :class:`Enum` members are instances of an :class:`Enum` class, and even though they are accessible as `EnumClass.member`, they should not be accessed directly from the member as that lookup may fail or, worse, return something -besides the :class:`Enum` member you looking for:: +besides the :class:`Enum` member you are looking for:: >>> class FieldTypes(Enum): ... name = 0 ... value = 1 ... size = 2 ... >>> FieldTypes.value.size >>> FieldTypes.size.value 2 diff --git a/Lib/enum.py b/Lib/enum.py --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,21 +1,24 @@ import sys from types import MappingProxyType, DynamicClassAttribute # try _collections first to reduce startup cost try: - from _collections import OrderedDict + from _collections import OrderedDict, defaultdict except ImportError: - from collections import OrderedDict + from collections import OrderedDict, defaultdictt -__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'unique'] +__all__ = [ + 'EnumMeta', 'Enum', 'IntEnum', 'AutoEnum', 'AutoNameEnum', 'UniqueEnum', + 'unique', 'AutoNumber', 'AutoName', 'Unique', + ] def _is_descriptor(obj): """Returns True if obj is a descriptor, False otherwise.""" return ( hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')) @@ -44,90 +47,229 @@ def _make_class_unpicklable(cls): class _EnumDict(dict): """Track enum member order and ensure member names are not reused. EnumMeta will use the names found in self._member_names as the enumeration member names. """ def __init__(self): - super().__init__() + super(_EnumDict, self).__init__() + # list of enum members self._member_names = [] + # starting value for AutoNumber + self._value = None + # when the magic turns off + self._locked = True + # list of temporary names + self._ignore = [] + # if _sunder_ values can be changed via the class body + self._init = True + # which auto, if any, is active + self._auto = None + # unique? + self._unique = False + + def __getitem__(self, key): + if ( + self._locked + or key in self + or key in self._ignore + or _is_sunder(key) + or _is_dunder(key) + ): + return super(_EnumDict, self).__getitem__(key) + if self._auto is AutoNumber: + try: + # try to generate the next value + value = self._value + 1 + self.__setitem__(key, value) + except: + # couldn't work the magic, report error + raise ValueError('Unable to generate value for %r from %r' % (key, self._value)) + elif self._auto is AutoName: + value = key.lower() + self.__setitem__(key, value) + else: + raise KeyError('%s not found' % key) + return value def __setitem__(self, key, value): """Changes anything not dundered or not a descriptor. If an enum member name is used twice, an error is raised; duplicate values are not checked for. Single underscore (sunder) names are reserved. """ if _is_sunder(key): - raise ValueError('_names_ are reserved for future Enum use') + if key not in ('_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_': + if isinstance(value, str): + value = value.split() + else: + value = list(value) + self._ignore = value + 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 AutoName in value and AutoNumber in value: + raise TypeError('cannot specify both AutoName and AutoNumber') + allowed_settings = dict.fromkeys(['autonumber', 'autoname', 'unique']) + for arg in value: + if arg not in allowed_settings: + raise TypeError('unknown setting: %r' % arg) + allowed_settings[arg] = True + if arg in (AutoName, AutoNumber): + self._auto = arg + self._locked = allowed_settings['autonumber'] and allowed_settings['autoname'] elif _is_dunder(key): - pass + if key == '__order__': + key = '_order_' + if _is_descriptor(value): + self._locked = True elif key in self._member_names: # descriptor overwriting an enum? raise TypeError('Attempted to reuse key: %r' % key) + elif key in self._ignore: + pass elif not _is_descriptor(value): + self._init = False if key in self: # enum overwriting a descriptor? - raise TypeError('Key already defined as: %r' % self[key]) + raise TypeError('%r already defined as: %r' % (key, self[key])) self._member_names.append(key) + if isinstance(value, int): + self._value = value + else: + # not a new member, turn off the autoassign magic + self._locked = True + self._init = False super().__setitem__(key, value) # Dummy value for Enum as EnumMeta explicitly checks for it, but of course # until EnumMeta finishes running the first time the Enum class doesn't exist. # This is also why there are checks in EnumMeta like `if Enum is not None` Enum = None +Unique = 'unique' +AutoName = 'autoname' +AutoNumber = 'autonumber' - +_ignore_sentinel = object() class EnumMeta(type): """Metaclass for Enum""" @classmethod - def __prepare__(metacls, cls, bases): - return _EnumDict() + def __prepare__(metacls, cls, bases, start=None, settings=(), ignore=_ignore_sentinel): + if not isinstance(settings, tuple): + settings = (settings, ) + # inherit previous flags + member_type, first_enum = metacls._get_mixins_(bases) + if first_enum is not None: + first_settings = getattr(first_enum, '_settings_', ()) + if start is None: + start = getattr(first_enum, '_start_', None) + else: + first_settings = () + enum_dict = _EnumDict() + # combine found flags and settings + settings = tuple(set(settings+first_settings)) + if AutoNumber in settings and start is None: + start = 1 + if settings: + # and add to class dict + enum_dict['_settings_'] = settings + if ignore is _ignore_sentinel: + enum_dict['_ignore_'] = 'property classmethod staticmethod'.split() + elif ignore: + enum_dict['_ignore_'] = ignore + if start is not None: + enum_dict['_start_'] = start + return enum_dict - def __new__(metacls, cls, bases, classdict): + def __init__(cls, *args , **kwds): + super(EnumMeta, cls).__init__(*args) + + def __new__(metacls, cls, bases, classdict, **kwds): # 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). member_type, first_enum = metacls._get_mixins_(bases) __new__, save_new, use_args = metacls._find_new_(classdict, member_type, first_enum) # save enum items into separate mapping so they don't get baked into # the new class - members = {k: classdict[k] for k in classdict._member_names} + enum_members = {k: classdict[k] for k in classdict._member_names} for name in classdict._member_names: del classdict[name] + # adjust the sunders + _settings_ = classdict.get('_settings_', ()) + _order_ = classdict.pop('_order_', None) + classdict.pop('_ignore_', None) + # maybe check for uniqueness of values + if Unique in _settings_: + value_map = defaultdict(list) + for member, value in enum_members.items(): + value_map[value].append(member) + dups = [] + for value, members in value_map.items(): + if len(members) > 1: + # put member names in definition order + members = [m for m in classdict._member_names if m in members] + dups.append('members %s all have the value of %r' % (', '.join([repr(m) for m in members]), value)) + if dups: + # use ValueError for consistency with unique decorator + raise ValueError('duplicate values found in %s:\n %s' % (cls, '\n '.join(dups))) + + # py3 support for definition order (helps keep py2/py3 code in sync) + if _order_ is not None: + if isinstance(_order_, str): + _order_ = _order_.replace(',', ' ').split() + 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_') + # check for illegal enum names (any others?) - invalid_names = set(members) & {'mro', } + invalid_names = set(enum_members) & {'mro', } if invalid_names: raise ValueError('Invalid enum member name: {0}'.format( ','.join(invalid_names))) # create a default docstring if one has not been provided if '__doc__' not in classdict: classdict['__doc__'] = 'An enumeration.' # create our new Enum type enum_class = super().__new__(metacls, cls, bases, classdict) enum_class._member_names_ = [] # names in definition order enum_class._member_map_ = OrderedDict() # name->value map enum_class._member_type_ = member_type + _start_ = classdict._value + if _start_ is not None: + # should be number of next member + _start_ += 1 + enum_class._start_ = _start_ # save attributes from super classes so we know if we can take # the shortcut of storing members in the class dict base_attributes = {a for b in enum_class.mro() for a in b.__dict__} # Reverse value->name map for hashable values. enum_class._value2member_map_ = {} # If a custom type is mixed into the Enum, and it does not know how # to pickle itself, pickle.dumps will succeed but pickle.loads will @@ -144,21 +286,21 @@ class EnumMeta(type): methods = ('__getnewargs_ex__', '__getnewargs__', '__reduce_ex__', '__reduce__') if not any(m in member_type.__dict__ for m in methods): _make_class_unpicklable(enum_class) # instantiate them, checking for duplicates as we go # we instantiate first instead of checking for duplicates first in case # a custom __new__ is doing something funky with the values -- such as # auto-numbering ;) for member_name in classdict._member_names: - value = members[member_name] + value = enum_members[member_name] if not isinstance(value, tuple): args = (value, ) else: args = value if member_type is tuple: # special case for tuple enums args = (args, ) # wrap it one more time if not use_args: enum_member = __new__(enum_class) if not hasattr(enum_member, '_value_'): enum_member._value_ = value @@ -565,20 +707,36 @@ class Enum(metaclass=EnumMeta): return cls class IntEnum(int, Enum): """Enum where members are also (and must be) ints""" def _reduce_ex_by_name(self, proto): return self.name +class _EnumConstants(str, Enum): + """EnumConstants for specifying Enum settings.""" + AutoNumber = 'autonumber' + AutoName = 'autoname' + Unique = 'unique' +globals().update(_EnumConstants.__members__) + +class AutoEnum(Enum, settings=AutoNumber): + """Enum where values are automatically assigned.""" + +class AutoNameEnum(Enum, settings=AutoName): + """Enum where member values are lower-cased versions of the name.""" + +class UniqueEnum(Enum, settings=Unique): + """Enum where all values must be unique (no aliases allowed).""" + def unique(enumeration): """Class decorator for enumerations ensuring unique member values.""" duplicates = [] for name, member in enumeration.__members__.items(): if name != member.name: duplicates.append((name, member.name)) if duplicates: alias_details = ', '.join( ["%s -> %s" % (alias, name) for (alias, name) in duplicates]) raise ValueError('duplicate values found in %r: %s' % diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1,16 +1,16 @@ import enum import inspect import pydoc import unittest from collections import OrderedDict -from enum import Enum, IntEnum, EnumMeta, unique +from enum import Enum, IntEnum, AutoEnum, AutoNameEnum, UniqueEnum, EnumMeta, unique from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support # for pickle tests try: class Stooges(Enum): LARRY = 1 CURLY = 2 MOE = 3 @@ -1564,22 +1564,198 @@ class TestEnum(unittest.TestCase): class LabelledList(LabelledIntEnum): 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) +class TestAutoNumber(unittest.TestCase): + + def test_autonumbering(self): + class Color(AutoEnum): + red + green + blue + self.assertEqual(list(Color), [Color.red, Color.green, Color.blue]) + self.assertEqual(Color.red.value, 1) + self.assertEqual(Color.green.value, 2) + self.assertEqual(Color.blue.value, 3) + + def test_autointnumbering(self): + class Color(int, AutoEnum): + red + green + blue + self.assertTrue(isinstance(Color.red, int)) + self.assertEqual(Color.green, 2) + self.assertTrue(Color.blue > Color.red) + + def test_badly_overridden_ignore(self): + with self.assertRaisesRegex(TypeError, "'int' object is not callable"): + class Color(AutoEnum): + _ignore_ = () + red + green + blue + @property + def whatever(self): + pass + with self.assertRaisesRegex(TypeError, "'int' object is not callable"): + class Color(AutoEnum, ignore=None): + red + green + blue + @property + def whatever(self): + pass + + def test_property(self): + class Color(AutoEnum): + _ignore_ = 'property' + red + green + blue + @property + def cap_name(self): + return self.name.title() + self.assertEqual(Color.blue.cap_name, 'Blue') + + def test_magic_turns_off(self): + with self.assertRaises(NameError): + class Color(AutoEnum): + _ignore_ = 'property' + red + green + blue + @property + def cap_name(self): + return self.name.title() + brown + + with self.assertRaises(NameError): + class Color(Enum, settings=AutoNumber): + _ignore_ = 'property' + red + green + blue + def hello(self): + print('Hello! My serial is %s.' % self.value) + rose + + with self.assertRaises(NameError): + class Color(Enum, settings=AutoNumber): + _ignore_ = 'property' + red + green + blue + def __init__(self, *args): + pass + rose + + +class TestAutoName(unittest.TestCase): + + def test_autonaming(self): + class Color(AutoNameEnum): + Red + Green + Blue + self.assertEqual(list(Color), [Color.Red, Color.Green, Color.Blue]) + self.assertEqual(Color.Red.value, 'red') + self.assertEqual(Color.Green.value, 'green') + self.assertEqual(Color.Blue.value, 'blue') + + def test_autonamestr(self): + class Color(str, AutoNameEnum): + Red + Green + Blue + self.assertTrue(isinstance(Color.Red, str)) + self.assertEqual(Color.Green, 'green') + self.assertTrue(Color.Blue < Color.Red) + + def test_overridden_ignore(self): + with self.assertRaisesRegex(TypeError, "'str' object is not callable"): + class Color(AutoNameEnum): + _ignore_ = () + red + green + blue + @property + def whatever(self): + pass + with self.assertRaisesRegex(TypeError, "'str' object is not callable"): + class Color(AutoNameEnum, ignore=None): + red + green + blue + @property + def whatever(self): + pass + + def test_property(self): + class Color(AutoNameEnum): + red + green + blue + @property + def upper_name(self): + return self.name.upper() + self.assertEqual(Color.blue.upper_name, 'BLUE') + + def test_magic_turns_off(self): + with self.assertRaises(NameError): + class Color(AutoNameEnum): + red + green + blue + @property + def cap_name(self): + return self.name.title() + brown + + with self.assertRaises(NameError): + class Color(Enum, settings=AutoNumber): + red + green + blue + def hello(self): + print('Hello! My value %s.' % self.value) + rose + + with self.assertRaises(NameError): + class Color(Enum, settings=AutoNumber): + red + green + blue + def __init__(self, *args): + pass + rose + + class TestUnique(unittest.TestCase): + def test_unique_in_settings(self): + class Nice(UniqueEnum): + red = 1 + green = 2 + blue = 3 + with self.assertRaisesRegex(ValueError, 'all have the value of 2'): + class NotNice(UniqueEnum): + red = 1 + green = 2 + blue = 3 + grene = 2 + def test_unique_clean(self): @unique class Clean(Enum): one = 1 two = 'dos' tres = 4.0 @unique class Cleaner(IntEnum): single = 1 double = 2