[Python-checkins] cpython: Add AutoEnum: automatically provides next value if missing. Issue 26988.

ethan.furman python-checkins at python.org
Fri Aug 5 19:18:42 EDT 2016


https://hg.python.org/cpython/rev/7ed7d7f58fcd
changeset:   102549:7ed7d7f58fcd
user:        Ethan Furman <ethan at stoneleaf.us>
date:        Fri Aug 05 16:03:16 2016 -0700
summary:
  Add AutoEnum: automatically provides next value if missing.  Issue 26988.

files:
  Doc/library/enum.rst  |  279 +++++++++++++++++++-----
  Lib/enum.py           |  133 +++++++++++-
  Lib/test/test_enum.py |  324 +++++++++++++++++++++++++++++-
  Misc/NEWS             |    2 +
  4 files changed, 661 insertions(+), 77 deletions(-)


diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -37,6 +37,13 @@
     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
+
 .. function:: unique
 
     Enum class decorator that ensures only one name is bound to any one value.
@@ -47,14 +54,14 @@
 
 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
@@ -72,6 +79,33 @@
     are not normal Python classes.  See `How are Enums different?`_ for
     more details.
 
+To create your own automatic :class:`Enum` classes, you need to add a
+:meth:`_generate_next_value_` method; it will be used to create missing values
+for any members after its definition.
+
+.. versionadded:: 3.6
+
+If you need full control of the member values, 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 the following 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)
@@ -235,7 +269,11 @@
 The ``__members__`` attribute can be used for detailed programmatic access to
 the enumeration members.  For example, finding all the aliases::
 
-    >>> [name for name, member in Shape.__members__.items() if member.name != name]
+    >>> [
+    ...   name
+    ...   for name, member in Shape.__members__.items()
+    ...   if member.name != name
+    ...   ]
     ['alias_for_square']
 
 
@@ -257,7 +295,7 @@
     >>> Color.red < Color.blue
     Traceback (most recent call last):
       File "<stdin>", line 1, in <module>
-    TypeError: unorderable types: Color() < Color()
+    TypeError: '<' not supported between instances of 'Color' and 'Color'
 
 Equality comparisons are defined though::
 
@@ -280,10 +318,10 @@
 ----------------------------------------------
 
 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::
@@ -393,17 +431,21 @@
     >>> list(Animal)
     [<Animal.ant: 1>, <Animal.bee: 2>, <Animal.cat: 3>, <Animal.dog: 4>]
 
-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
@@ -419,7 +461,7 @@
 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__)
@@ -439,7 +481,15 @@
 
 The complete signature is::
 
-    Enum(value='NewEnumName', names=<...>, *, module='...', qualname='...', type=<mixed-in class>, start=1)
+    Enum(
+        value='NewEnumName',
+        names=<...>,
+        *,
+        module='...',
+        qualname='...',
+        type=<mixed-in class>,
+        start=1,
+        )
 
 :value: What the new Enum class will record as its name.
 
@@ -475,10 +525,41 @@
 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)
+    [<Color.red: 1>, <Color.green: 5>, <Color.blue: 6>]
+
+.. 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 parameter::
+
+        >>> class AddressType(AutoEnum, ignore='classmethod staticmethod'):
+        ...     pobox
+        ...     mailbox
+        ...     property
+        ...
+
+.. 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::
@@ -521,14 +602,13 @@
     >>> [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
-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.
-
+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 ``IntEnum`` 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.
 
 Others
 ^^^^^^
@@ -540,7 +620,9 @@
         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:`AutoIntEnum` that mixes in :class:`int` with :class:`AutoEnum`
+to get members that are :class:`int` (but keep in mind the warnings for
+:class:`IntEnum`).
 
 Some rules:
 
@@ -567,31 +649,35 @@
 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::
+Automatically numbers the members, and uses the given value as the
+:attr:`__doc__` string::
 
-    >>> 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::
 
@@ -599,6 +685,23 @@
     members; it is then replaced by Enum's :meth:`__new__` which is used after
     class creation for lookup of existing members.
 
+AutoNameEnum
+^^^^^^^^^^^^
+
+Automatically sets the member's value to its name::
+
+    >>> class AutoNameEnum(Enum):
+    ...     def _generate_next_value_(name, start, count, last_value):
+    ...         return name
+    ...
+    >>> class Color(AutoNameEnum):
+    ...     red
+    ...     green
+    ...     blue
+    ...
+    >>> Color.green.value == 'green'
+    True
+
 
 OrderedEnum
 ^^^^^^^^^^^
@@ -731,10 +834,61 @@
 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
+Enum class signature
+~~~~~~~~~~~~~~~~~~~~
+
+    ``class SomeName(
+            AnEnum,
+            start=None,
+            ignore='staticmethod classmethod property',
+            ):``
+
+``start`` can be used by a :meth:`_generate_next_value_` method to specify a
+starting value.
+
+``ignore`` specifies which names, if any, will not attempt to auto-generate
+a new value (they will also be removed from the class body).
+
+
+Supported ``__dunder__`` names
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :attr:`__members__` attribute is only available on the class.
+
+:meth:`__new__`, if specified, must create and return the enum members; it is
+also a very good idea to set the member's :attr:`_value_` appropriately.  Once
+all the members are created it is no longer used.
+
+
+Supported ``_sunder_`` names
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent [class attribute]
+
+- ``_name_`` -- name of the member (but use ``name`` for normal access)
+- ``_value_`` -- value of the member; can be set / modified in ``__new__`` (see ``_name_``)
+- ``_missing_`` -- a lookup function used when a value is not found (only after class creation)
+- ``_generate_next_value_`` -- a function to generate missing values (only during class creation)
+
+
+:meth:`_generate_next_value_` signature
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    ``def _generate_next_value_(name, start, count, last_value):``
+
+- ``name`` is the name of the member
+- ``start`` is the initital start value (if any) or None
+- ``count`` is the number of existing members in the enumeration
+- ``last_value`` is the value of the last enum member (if any) or None
+
+
+Enum member type
+~~~~~~~~~~~~~~~~
+
+``Enum`` members are instances of an ``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 ``Enum`` member you are looking for::
 
     >>> class FieldTypes(Enum):
     ...     name = 0
@@ -748,18 +902,24 @@
 
 .. versionchanged:: 3.5
 
-Boolean evaluation: Enum classes that are mixed with non-Enum types (such as
+
+Boolean value of ``Enum`` classes and members
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Enum classes that are mixed with non-Enum types (such as
 :class:`int`, :class:`str`, etc.) are evaluated according to the mixed-in
-type's rules; otherwise, all members evaluate as ``True``.  To make your own
+type's rules; otherwise, all members evaluate as :data:`True`.  To make your own
 Enum's boolean evaluation depend on the member's value add the following to
 your class::
 
     def __bool__(self):
         return bool(self.value)
 
-The :attr:`__members__` attribute is only available on the class.
 
-If you give your :class:`Enum` subclass extra methods, like the `Planet`_
+Enum classes with methods
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you give your ``Enum`` subclass extra methods, like the `Planet`_
 class above, those methods will show up in a :func:`dir` of the member,
 but not of the class::
 
@@ -767,12 +927,3 @@
     ['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
     >>> dir(Planet.EARTH)
     ['__class__', '__doc__', '__module__', 'name', 'surface_gravity', 'value']
-
-The :meth:`__new__` method will only be used for the creation of the
-:class:`Enum` members -- after that it is replaced.  Any custom :meth:`__new__`
-method must create the object and set the :attr:`_value_` attribute
-appropriately.
-
-If you wish to change how :class:`Enum` members are looked up you should either
-write a helper function or a :func:`classmethod` for the :class:`Enum`
-subclass.
diff --git a/Lib/enum.py b/Lib/enum.py
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -8,7 +8,9 @@
     from collections import OrderedDict
 
 
-__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'unique']
+__all__ = [
+        'EnumMeta', 'Enum', 'IntEnum', 'AutoEnum', 'unique',
+        ]
 
 
 def _is_descriptor(obj):
@@ -52,7 +54,30 @@
     """
     def __init__(self):
         super().__init__()
+        # list of enum members
         self._member_names = []
+        # starting value
+        self._start = None
+        # last assigned value
+        self._last_value = None
+        # when the magic turns off
+        self._locked = True
+        # list of temporary names
+        self._ignore = []
+
+    def __getitem__(self, key):
+        if (
+                self._generate_next_value_ is None
+                or 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)
+        next_value = self._generate_next_value_(key, self._start, len(self._member_names), self._last_value)
+        self[key] = next_value
+        return next_value
 
     def __setitem__(self, key, value):
         """Changes anything not dundered or not a descriptor.
@@ -64,19 +89,55 @@
 
         """
         if _is_sunder(key):
-            raise ValueError('_names_ are reserved for future Enum use')
+            if key not in ('_settings_', '_order_', '_ignore_', '_start_', '_generate_next_value_'):
+                raise ValueError('_names_ are reserved for future Enum use')
+            elif key == '_generate_next_value_':
+                if isinstance(value, staticmethod):
+                    value = value.__get__(None, self)
+                self._generate_next_value_ = value
+                self._locked = False
+            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._start = value
+                self._locked = False
         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):
             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 self._generate_next_value_ is not None:
+                self._last_value = value
+        else:
+            # not a new member, turn off the autoassign magic
+            self._locked = True
         super().__setitem__(key, value)
 
+    # for magic "auto values" an Enum class should specify a `_generate_next_value_`
+    # method; that method will be used to generate missing values, and is
+    # implicitly a staticmethod;
+    # the signature should be `def _generate_next_value_(name, last_value)`
+    # last_value will be the last value created and/or assigned, or None
+    _generate_next_value_ = None
 
 
 # Dummy value for Enum as EnumMeta explicitly checks for it, but of course
@@ -84,14 +145,31 @@
 # This is also why there are checks in EnumMeta like `if Enum is not None`
 Enum = None
 
-
+_ignore_sentinel = object()
 class EnumMeta(type):
     """Metaclass for Enum"""
     @classmethod
-    def __prepare__(metacls, cls, bases):
-        return _EnumDict()
+    def __prepare__(metacls, cls, bases, start=None, ignore=_ignore_sentinel):
+        # create the namespace dict
+        enum_dict = _EnumDict()
+        # inherit previous flags and _generate_next_value_ function
+        member_type, first_enum = metacls._get_mixins_(bases)
+        if first_enum is not None:
+            enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None)
+            if start is None:
+                start = getattr(first_enum, '_start_', None)
+        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
@@ -102,12 +180,24 @@
 
         # 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
+        _order_ = classdict.pop('_order_', None)
+        classdict.pop('_ignore_', None)
+
+        # 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)))
@@ -151,7 +241,7 @@
         # 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:
@@ -165,7 +255,10 @@
             else:
                 enum_member = __new__(enum_class, *args)
                 if not hasattr(enum_member, '_value_'):
-                    enum_member._value_ = member_type(*args)
+                    if member_type is object:
+                        enum_member._value_ = value
+                    else:
+                        enum_member._value_ = member_type(*args)
             value = enum_member._value_
             enum_member._name_ = member_name
             enum_member.__objclass__ = enum_class
@@ -572,6 +665,22 @@
 def _reduce_ex_by_name(self, proto):
     return self.name
 
+class AutoEnum(Enum):
+    """Enum where values are automatically assigned."""
+    def _generate_next_value_(name, start, count, last_value):
+        """
+        Generate the next value when not given.
+
+        name: the name of the member
+        start: the initital start value or None
+        count: the number of existing members
+        last_value: the last value assigned or None
+        """
+        # add one to the last assigned value
+        if not count:
+            return start if start is not None else 1
+        return last_value + 1
+
 def unique(enumeration):
     """Class decorator for enumerations ensuring unique member values."""
     duplicates = []
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
@@ -3,7 +3,7 @@
 import pydoc
 import unittest
 from collections import OrderedDict
-from enum import Enum, IntEnum, EnumMeta, unique
+from enum import EnumMeta, Enum, IntEnum, AutoEnum, unique
 from io import StringIO
 from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
 from test import support
@@ -1570,6 +1570,328 @@
         self.assertEqual(LabelledList.unprocessed, 1)
         self.assertEqual(LabelledList(1), LabelledList.unprocessed)
 
+    def test_ignore_as_str(self):
+        from datetime import timedelta
+        class Period(Enum, ignore='Period i'):
+            """
+            different lengths of time
+            """
+            def __new__(cls, value, period):
+                obj = object.__new__(cls)
+                obj._value_ = value
+                obj.period = period
+                return obj
+            Period = vars()
+            for i in range(367):
+                Period['Day%d' % i] = timedelta(days=i), 'day'
+            for i in range(53):
+                Period['Week%d' % i] = timedelta(days=i*7), 'week'
+            for i in range(13):
+                Period['Month%d' % i] = i, 'month'
+            OneDay = Day1
+            OneWeek = Week1
+        self.assertEqual(Period.Day7.value, timedelta(days=7))
+        self.assertEqual(Period.Day7.period, 'day')
+
+    def test_ignore_as_list(self):
+        from datetime import timedelta
+        class Period(Enum, ignore=['Period', 'i']):
+            """
+            different lengths of time
+            """
+            def __new__(cls, value, period):
+                obj = object.__new__(cls)
+                obj._value_ = value
+                obj.period = period
+                return obj
+            Period = vars()
+            for i in range(367):
+                Period['Day%d' % i] = timedelta(days=i), 'day'
+            for i in range(53):
+                Period['Week%d' % i] = timedelta(days=i*7), 'week'
+            for i in range(13):
+                Period['Month%d' % i] = i, 'month'
+            OneDay = Day1
+            OneWeek = Week1
+        self.assertEqual(Period.Day7.value, timedelta(days=7))
+        self.assertEqual(Period.Day7.period, 'day')
+
+    def test_new_with_no_value_and_int_base_class(self):
+        class NoValue(int, Enum):
+            def __new__(cls, value):
+                obj = int.__new__(cls, value)
+                obj.index = len(cls.__members__)
+                return obj
+            this = 1
+            that = 2
+        self.assertEqual(list(NoValue), [NoValue.this, NoValue.that])
+        self.assertEqual(NoValue.this, 1)
+        self.assertEqual(NoValue.this.value, 1)
+        self.assertEqual(NoValue.this.index, 0)
+        self.assertEqual(NoValue.that, 2)
+        self.assertEqual(NoValue.that.value, 2)
+        self.assertEqual(NoValue.that.index, 1)
+
+    def test_new_with_no_value(self):
+        class NoValue(Enum):
+            def __new__(cls, value):
+                obj = object.__new__(cls)
+                obj.index = len(cls.__members__)
+                return obj
+            this = 1
+            that = 2
+        self.assertEqual(list(NoValue), [NoValue.this, NoValue.that])
+        self.assertEqual(NoValue.this.value, 1)
+        self.assertEqual(NoValue.this.index, 0)
+        self.assertEqual(NoValue.that.value, 2)
+        self.assertEqual(NoValue.that.index, 1)
+
+
+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_autonumbering_with_start(self):
+        class Color(AutoEnum, start=7):
+            red
+            green
+            blue
+        self.assertEqual(list(Color), [Color.red, Color.green, Color.blue])
+        self.assertEqual(Color.red.value, 7)
+        self.assertEqual(Color.green.value, 8)
+        self.assertEqual(Color.blue.value, 9)
+
+    def test_autonumbering_with_start_and_skip(self):
+        class Color(AutoEnum, start=7):
+            red
+            green
+            blue = 11
+            brown
+        self.assertEqual(list(Color), [Color.red, Color.green, Color.blue, Color.brown])
+        self.assertEqual(Color.red.value, 7)
+        self.assertEqual(Color.green.value, 8)
+        self.assertEqual(Color.blue.value, 11)
+        self.assertEqual(Color.brown.value, 12)
+
+
+    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
+        with self.assertRaisesRegex(TypeError, "'int' object is not callable"):
+            class Color(AutoEnum, ignore='classmethod staticmethod'):
+                red
+                green
+                blue
+                @property
+                def whatever(self):
+                    pass
+
+    def test_property(self):
+        class Color(AutoEnum):
+            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.assertRaisesRegex(NameError, "brown"):
+            class Color(AutoEnum):
+                red
+                green
+                blue
+                @property
+                def cap_name(self):
+                    return self.name.title()
+                brown
+
+        with self.assertRaisesRegex(NameError, "rose"):
+            class Color(AutoEnum):
+                red
+                green
+                blue
+                def hello(self):
+                    print('Hello!  My serial is %s.' % self.value)
+                rose
+
+        with self.assertRaisesRegex(NameError, "cyan"):
+            class Color(AutoEnum):
+                red
+                green
+                blue
+                def __init__(self, *args):
+                    pass
+                cyan
+
+
+class TestGenerateMethod(unittest.TestCase):
+
+    def test_autonaming(self):
+        class Color(Enum):
+            def _generate_next_value_(name, start, count, last_value):
+                return name
+            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, Enum):
+            def _generate_next_value_(name, start, count, last_value):
+                return name
+            Red
+            Green
+            Blue
+        self.assertTrue(isinstance(Color.Red, str))
+        self.assertEqual(Color.Green, 'Green')
+        self.assertTrue(Color.Blue < Color.Red)
+
+    def test_generate_as_staticmethod(self):
+        class Color(str, Enum):
+            @staticmethod
+            def _generate_next_value_(name, start, count, last_value):
+                return name.lower()
+            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(Enum):
+                def _generate_next_value_(name, start, count, last_value):
+                    return name
+                _ignore_ = ()
+                red
+                green
+                blue
+                @property
+                def whatever(self):
+                    pass
+        with self.assertRaisesRegex(TypeError, "'str' object is not callable"):
+            class Color(Enum, ignore=None):
+                def _generate_next_value_(name, start, count, last_value):
+                    return name
+                red
+                green
+                blue
+                @property
+                def whatever(self):
+                    pass
+
+    def test_property(self):
+        class Color(Enum):
+            def _generate_next_value_(name, start, count, last_value):
+                return name
+            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.assertRaisesRegex(NameError, "brown"):
+            class Color(Enum):
+                def _generate_next_value_(name, start, count, last_value):
+                    return name
+                red
+                green
+                blue
+                @property
+                def cap_name(self):
+                    return self.name.title()
+                brown
+
+        with self.assertRaisesRegex(NameError, "rose"):
+            class Color(Enum):
+                def _generate_next_value_(name, start, count, last_value):
+                    return name
+                red
+                green
+                blue
+                def hello(self):
+                    print('Hello!  My value %s.' % self.value)
+                rose
+
+        with self.assertRaisesRegex(NameError, "cyan"):
+            class Color(Enum):
+                def _generate_next_value_(name, start, count, last_value):
+                    return name
+                red
+                green
+                blue
+                def __init__(self, *args):
+                    pass
+                cyan
+
+    def test_powers_of_two(self):
+        class Bits(Enum):
+            def _generate_next_value_(name, start, count, last_value):
+                return 2 ** count
+            one
+            two
+            four
+            eight
+        self.assertEqual(Bits.one.value, 1)
+        self.assertEqual(Bits.two.value, 2)
+        self.assertEqual(Bits.four.value, 4)
+        self.assertEqual(Bits.eight.value, 8)
+
+    def test_powers_of_two_as_int(self):
+        class Bits(int, Enum):
+            def _generate_next_value_(name, start, count, last_value):
+                return 2 ** count
+            one
+            two
+            four
+            eight
+        self.assertEqual(Bits.one, 1)
+        self.assertEqual(Bits.two, 2)
+        self.assertEqual(Bits.four, 4)
+        self.assertEqual(Bits.eight, 8)
+
 
 class TestUnique(unittest.TestCase):
 
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -78,6 +78,8 @@
 - Issue 27512: Fix a segfault when os.fspath() called a an __fspath__() method
   that raised an exception. Patch by Xiang Zhang.
 
+- Issue 26988: Add AutoEnum.
+
 Tests
 -----
 

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list