[Python-checkins] cpython: issue23591: add auto() for auto-generating Enum member values

ethan.furman python-checkins at python.org
Sun Sep 11 02:37:24 EDT 2016


https://hg.python.org/cpython/rev/aad7443e62be
changeset:   103609:aad7443e62be
user:        Ethan Furman <ethan at stoneleaf.us>
date:        Sat Sep 10 23:36:59 2016 -0700
summary:
  issue23591: add auto() for auto-generating Enum member values

files:
  Doc/library/enum.rst  |  99 +++++++++++++++++++++++++-----
  Lib/enum.py           |  50 +++++++++++----
  Lib/test/test_enum.py |  77 +++++++++++++++++++++++-
  3 files changed, 195 insertions(+), 31 deletions(-)


diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -25,7 +25,8 @@
 
 This module defines four enumeration classes that can be used to define unique
 sets of names and values: :class:`Enum`, :class:`IntEnum`, and
-:class:`IntFlags`.  It also defines one decorator, :func:`unique`.
+:class:`IntFlags`.  It also defines one decorator, :func:`unique`, and one
+helper, :class:`auto`.
 
 .. class:: Enum
 
@@ -52,7 +53,11 @@
 
     Enum class decorator that ensures only one name is bound to any one value.
 
-.. versionadded:: 3.6  ``Flag``, ``IntFlag``
+.. class:: auto
+
+    Instances are replaced with an appropriate value for Enum members.
+
+.. versionadded:: 3.6  ``Flag``, ``IntFlag``, ``auto``
 
 
 Creating an Enum
@@ -70,6 +75,13 @@
     ...     blue = 3
     ...
 
+.. note:: Enum member values
+
+    Member values can be anything: :class:`int`, :class:`str`, etc..  If
+    the exact value is unimportant you may use :class:`auto` instances and an
+    appropriate value will be chosen for you.  Care must be taken if you mix
+    :class:`auto` with other values.
+
 .. note:: Nomenclature
 
   - The class :class:`Color` is an *enumeration* (or *enum*)
@@ -225,6 +237,42 @@
     ValueError: duplicate values found in <enum 'Mistake'>: four -> three
 
 
+Using automatic values
+----------------------
+
+If the exact value is unimportant you can use :class:`auto`::
+
+    >>> from enum import Enum, auto
+    >>> class Color(Enum):
+    ...     red = auto()
+    ...     blue = auto()
+    ...     green = auto()
+    ...
+    >>> list(Color)
+    [<Color.red: 1>, <Color.blue: 2>, <Color.green: 3>]
+
+The values are chosen by :func:`_generate_next_value_`, which can be
+overridden::
+
+    >>> class AutoName(Enum):
+    ...     def _generate_next_value_(name, start, count, last_values):
+    ...         return name
+    ...
+    >>> class Ordinal(AutoName):
+    ...     north = auto()
+    ...     south = auto()
+    ...     east = auto()
+    ...     west = auto()
+    ...
+    >>> list(Ordinal)
+    [<Ordinal.north: 'north'>, <Ordinal.south: 'south'>, <Ordinal.east: 'east'>, <Ordinal.west: 'west'>]
+
+.. note::
+
+    The goal of the default :meth:`_generate_next_value_` methods is to provide
+    the next :class:`int` in sequence with the last :class:`int` provided, but
+    the way it does this is an implementation detail and may change.
+
 Iteration
 ---------
 
@@ -597,7 +645,9 @@
 The last variation is :class:`Flag`.  Like :class:`IntFlag`, :class:`Flag`
 members can be combined using the bitwise operators (&, \|, ^, ~).  Unlike
 :class:`IntFlag`, they cannot be combined with, nor compared against, any
-other :class:`Flag` enumeration, nor :class:`int`.
+other :class:`Flag` enumeration, nor :class:`int`.  While it is possible to
+specify the values directly it is recommended to use :class:`auto` as the
+value and let :class:`Flag` select an appropriate value.
 
 .. versionadded:: 3.6
 
@@ -606,9 +656,9 @@
 
     >>> from enum import Flag
     >>> class Color(Flag):
-    ...     red = 1
-    ...     blue = 2
-    ...     green = 4
+    ...     red = auto()
+    ...     blue = auto()
+    ...     green = auto()
     ...
     >>> Color.red & Color.green
     <Color.0: 0>
@@ -619,21 +669,20 @@
 while combinations of flags won't::
 
     >>> class Color(Flag):
-    ...     red = 1
-    ...     blue = 2
-    ...     green = 4
-    ...     white = 7
-    ...     # or
-    ...     # white = red | blue | green
+    ...     red = auto()
+    ...     blue = auto()
+    ...     green = auto()
+    ...     white = red | blue | green
+    ...
 
 Giving a name to the "no flags set" condition does not change its boolean
 value::
 
     >>> class Color(Flag):
     ...     black = 0
-    ...     red = 1
-    ...     blue = 2
-    ...     green = 4
+    ...     red = auto()
+    ...     blue = auto()
+    ...     green = auto()
     ...
     >>> Color.black
     <Color.black: 0>
@@ -700,6 +749,7 @@
 In many use-cases one doesn't care what the actual value of an enumeration
 is. There are several ways to define this type of simple enumeration:
 
+- use instances of :class:`auto` for the value
 - use instances of :class:`object` as the value
 - use a descriptive string as the value
 - use a tuple as the value and a custom :meth:`__new__` to replace the
@@ -718,6 +768,20 @@
     ...
 
 
+Using :class:`auto`
+"""""""""""""""""""
+
+Using :class:`object` would look like::
+
+    >>> class Color(NoValue):
+    ...     red = auto()
+    ...     blue = auto()
+    ...     green = auto()
+    ...
+    >>> Color.green
+    <Color.green>
+
+
 Using :class:`object`
 """""""""""""""""""""
 
@@ -930,8 +994,11 @@
   overridden
 - ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
   (class attribute, removed during class creation)
+- ``_generate_next_value_`` -- used by the `Functional API`_ and by
+  :class:`auto` to get an appropriate value for an enum member; may be
+  overridden
 
-.. versionadded:: 3.6 ``_missing_``, ``_order_``
+.. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
 
 To help keep Python 2 / Python 3 code in sync an :attr:`_order_` attribute can
 be provided.  It will be checked against the actual order of the enumeration
diff --git a/Lib/enum.py b/Lib/enum.py
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -10,7 +10,11 @@
     from collections import OrderedDict
 
 
-__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'unique']
+__all__ = [
+        'EnumMeta',
+        'Enum', 'IntEnum', 'Flag', 'IntFlag',
+        'auto', 'unique',
+        ]
 
 
 def _is_descriptor(obj):
@@ -36,7 +40,6 @@
             name[-2:-1] != '_' and
             len(name) > 2)
 
-
 def _make_class_unpicklable(cls):
     """Make the given class un-picklable."""
     def _break_on_call_reduce(self, proto):
@@ -44,6 +47,12 @@
     cls.__reduce_ex__ = _break_on_call_reduce
     cls.__module__ = '<unknown>'
 
+class auto:
+    """
+    Instances are replaced with an appropriate value in Enum class suites.
+    """
+    pass
+
 
 class _EnumDict(dict):
     """Track enum member order and ensure member names are not reused.
@@ -55,6 +64,7 @@
     def __init__(self):
         super().__init__()
         self._member_names = []
+        self._last_values = []
 
     def __setitem__(self, key, value):
         """Changes anything not dundered or not a descriptor.
@@ -71,6 +81,8 @@
                     '_generate_next_value_', '_missing_',
                     ):
                 raise ValueError('_names_ are reserved for future Enum use')
+            if key == '_generate_next_value_':
+                setattr(self, '_generate_next_value', value)
         elif _is_dunder(key):
             if key == '__order__':
                 key = '_order_'
@@ -81,11 +93,13 @@
             if key in self:
                 # enum overwriting a descriptor?
                 raise TypeError('%r already defined as: %r' % (key, self[key]))
+            if isinstance(value, auto):
+                value = self._generate_next_value(key, 1, len(self._member_names), self._last_values[:])
             self._member_names.append(key)
+            self._last_values.append(value)
         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`
@@ -366,10 +380,11 @@
             names = names.replace(',', ' ').split()
         if isinstance(names, (tuple, list)) and isinstance(names[0], str):
             original_names, names = names, []
-            last_value = None
+            last_values = []
             for count, name in enumerate(original_names):
-                last_value = first_enum._generate_next_value_(name, start, count, last_value)
-                names.append((name, last_value))
+                value = first_enum._generate_next_value_(name, start, count, last_values[:])
+                last_values.append(value)
+                names.append((name, value))
 
         # Here, names is either an iterable of (name, value) or a mapping.
         for item in names:
@@ -514,11 +529,15 @@
         # still not found -- try _missing_ hook
         return cls._missing_(value)
 
-    @staticmethod
-    def _generate_next_value_(name, start, count, last_value):
-        if not count:
+    def _generate_next_value_(name, start, count, last_values):
+        for last_value in reversed(last_values):
+            try:
+                return last_value + 1
+            except TypeError:
+                pass
+        else:
             return start
-        return last_value + 1
+
     @classmethod
     def _missing_(cls, value):
         raise ValueError("%r is not a valid %s" % (value, cls.__name__))
@@ -616,8 +635,8 @@
 
 class Flag(Enum):
     """Support for flags"""
-    @staticmethod
-    def _generate_next_value_(name, start, count, last_value):
+
+    def _generate_next_value_(name, start, count, last_values):
         """
         Generate the next value when not given.
 
@@ -628,7 +647,12 @@
         """
         if not count:
             return start if start is not None else 1
-        high_bit = _high_bit(last_value)
+        for last_value in reversed(last_values):
+            try:
+                high_bit = _high_bit(last_value)
+                break
+            except TypeError:
+                raise TypeError('Invalid Flag value: %r' % last_value) from None
         return 2 ** (high_bit+1)
 
     @classmethod
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, Flag, IntFlag, unique
+from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto
 from io import StringIO
 from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
 from test import support
@@ -113,6 +113,7 @@
                 '__', '___', '____', '_____',):
             self.assertFalse(enum._is_dunder(s))
 
+# tests
 
 class TestEnum(unittest.TestCase):
 
@@ -1578,6 +1579,61 @@
         self.assertEqual(LabelledList.unprocessed, 1)
         self.assertEqual(LabelledList(1), LabelledList.unprocessed)
 
+    def test_auto_number(self):
+        class Color(Enum):
+            red = auto()
+            blue = auto()
+            green = auto()
+
+        self.assertEqual(list(Color), [Color.red, Color.blue, Color.green])
+        self.assertEqual(Color.red.value, 1)
+        self.assertEqual(Color.blue.value, 2)
+        self.assertEqual(Color.green.value, 3)
+
+    def test_auto_name(self):
+        class Color(Enum):
+            def _generate_next_value_(name, start, count, last):
+                return name
+            red = auto()
+            blue = auto()
+            green = auto()
+
+        self.assertEqual(list(Color), [Color.red, Color.blue, Color.green])
+        self.assertEqual(Color.red.value, 'red')
+        self.assertEqual(Color.blue.value, 'blue')
+        self.assertEqual(Color.green.value, 'green')
+
+    def test_auto_name_inherit(self):
+        class AutoNameEnum(Enum):
+            def _generate_next_value_(name, start, count, last):
+                return name
+        class Color(AutoNameEnum):
+            red = auto()
+            blue = auto()
+            green = auto()
+
+        self.assertEqual(list(Color), [Color.red, Color.blue, Color.green])
+        self.assertEqual(Color.red.value, 'red')
+        self.assertEqual(Color.blue.value, 'blue')
+        self.assertEqual(Color.green.value, 'green')
+
+    def test_auto_garbage(self):
+        class Color(Enum):
+            red = 'red'
+            blue = auto()
+        self.assertEqual(Color.blue.value, 1)
+
+    def test_auto_garbage_corrected(self):
+        class Color(Enum):
+            red = 'red'
+            blue = 2
+            green = auto()
+
+        self.assertEqual(list(Color), [Color.red, Color.blue, Color.green])
+        self.assertEqual(Color.red.value, 'red')
+        self.assertEqual(Color.blue.value, 2)
+        self.assertEqual(Color.green.value, 3)
+
 
 class TestOrder(unittest.TestCase):
 
@@ -1856,7 +1912,6 @@
         test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE)
         test_pickle_dump_load(self.assertIs, FlagStooges)
 
-
     def test_containment(self):
         Perm = self.Perm
         R, W, X = Perm
@@ -1877,6 +1932,24 @@
         self.assertFalse(W in RX)
         self.assertFalse(X in RW)
 
+    def test_auto_number(self):
+        class Color(Flag):
+            red = auto()
+            blue = auto()
+            green = auto()
+
+        self.assertEqual(list(Color), [Color.red, Color.blue, Color.green])
+        self.assertEqual(Color.red.value, 1)
+        self.assertEqual(Color.blue.value, 2)
+        self.assertEqual(Color.green.value, 4)
+
+    def test_auto_number_garbage(self):
+        with self.assertRaisesRegex(TypeError, 'Invalid Flag value: .not an int.'):
+            class Color(Flag):
+                red = 'not an int'
+                blue = auto()
+
+
 class TestIntFlag(unittest.TestCase):
     """Tests of the IntFlags."""
 

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


More information about the Python-checkins mailing list