summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEthan Furman <ethan@stoneleaf.us>2016-09-11 06:36:59 (GMT)
committerEthan Furman <ethan@stoneleaf.us>2016-09-11 06:36:59 (GMT)
commitc16595e567d51a1773be30c34622620b52be7acf (patch)
tree3126fe0d721c7eb7b6eb279d20e3f7ea79c6bc63
parent944368e1cc90a0bebaaf1a0a6f4346a81d8f46ad (diff)
downloadcpython-c16595e567d51a1773be30c34622620b52be7acf.zip
cpython-c16595e567d51a1773be30c34622620b52be7acf.tar.gz
cpython-c16595e567d51a1773be30c34622620b52be7acf.tar.bz2
issue23591: add auto() for auto-generating Enum member values
-rw-r--r--Doc/library/enum.rst99
-rw-r--r--Lib/enum.py50
-rw-r--r--Lib/test/test_enum.py77
3 files changed, 195 insertions, 31 deletions
diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index 61679e1..eb8b94b 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -25,7 +25,8 @@ Module Contents
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 @@ sets of names and values: :class:`Enum`, :class:`IntEnum`, and
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 @@ follows::
... 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 @@ found :exc:`ValueError` is raised with the details::
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 @@ Flag
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 @@ flags being set, the boolean evaluation is :data:`False`::
>>> 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 @@ Individual flags should have values that are powers of two (1, 2, 4, 8, ...),
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 @@ Omitting values
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 @@ the (unimportant) value::
...
+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 @@ Supported ``_sunder_`` names
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
index 6a18999..1f87664 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -10,7 +10,11 @@ except ImportError:
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 @@ def _is_sunder(name):
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 @@ def _make_class_unpicklable(cls):
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 @@ class _EnumDict(dict):
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 @@ class _EnumDict(dict):
'_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 @@ class _EnumDict(dict):
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 @@ class EnumMeta(type):
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 @@ class Enum(metaclass=EnumMeta):
# 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 @@ def _reduce_ex_by_name(self, proto):
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 @@ class Flag(Enum):
"""
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
index 698fd30..153bfb4 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -3,7 +3,7 @@ import inspect
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 @@ class TestHelpers(unittest.TestCase):
'__', '___', '____', '_____',):
self.assertFalse(enum._is_dunder(s))
+# tests
class TestEnum(unittest.TestCase):
@@ -1578,6 +1579,61 @@ class TestEnum(unittest.TestCase):
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 @@ class TestFlag(unittest.TestCase):
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 @@ class TestFlag(unittest.TestCase):
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."""