issue23591: add auto() for auto-generating Enum member values

This commit is contained in:
Ethan Furman 2016-09-10 23:36:59 -07:00
parent 944368e1cc
commit c16595e567
3 changed files with 195 additions and 31 deletions

View File

@ -25,7 +25,8 @@ Module Contents
This module defines four enumeration classes that can be used to define unique This module defines four enumeration classes that can be used to define unique
sets of names and values: :class:`Enum`, :class:`IntEnum`, and 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 .. 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. 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 Creating an Enum
@ -70,6 +75,13 @@ follows::
... blue = 3 ... 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 .. note:: Nomenclature
- The class :class:`Color` is an *enumeration* (or *enum*) - 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 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 Iteration
--------- ---------
@ -597,7 +645,9 @@ Flag
The last variation is :class:`Flag`. Like :class:`IntFlag`, :class:`Flag` The last variation is :class:`Flag`. Like :class:`IntFlag`, :class:`Flag`
members can be combined using the bitwise operators (&, \|, ^, ~). Unlike members can be combined using the bitwise operators (&, \|, ^, ~). Unlike
:class:`IntFlag`, they cannot be combined with, nor compared against, any :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 .. versionadded:: 3.6
@ -606,9 +656,9 @@ flags being set, the boolean evaluation is :data:`False`::
>>> from enum import Flag >>> from enum import Flag
>>> class Color(Flag): >>> class Color(Flag):
... red = 1 ... red = auto()
... blue = 2 ... blue = auto()
... green = 4 ... green = auto()
... ...
>>> Color.red & Color.green >>> Color.red & Color.green
<Color.0: 0> <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:: while combinations of flags won't::
>>> class Color(Flag): >>> class Color(Flag):
... red = 1 ... red = auto()
... blue = 2 ... blue = auto()
... green = 4 ... green = auto()
... white = 7 ... white = red | blue | green
... # or ...
... # white = red | blue | green
Giving a name to the "no flags set" condition does not change its boolean Giving a name to the "no flags set" condition does not change its boolean
value:: value::
>>> class Color(Flag): >>> class Color(Flag):
... black = 0 ... black = 0
... red = 1 ... red = auto()
... blue = 2 ... blue = auto()
... green = 4 ... green = auto()
... ...
>>> Color.black >>> Color.black
<Color.black: 0> <Color.black: 0>
@ -700,6 +749,7 @@ Omitting values
In many use-cases one doesn't care what the actual value of an enumeration 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: 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 instances of :class:`object` as the value
- use a descriptive string as the value - use a descriptive string as the value
- use a tuple as the value and a custom :meth:`__new__` to replace the - 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` Using :class:`object`
""""""""""""""""""""" """""""""""""""""""""
@ -930,8 +994,11 @@ Supported ``_sunder_`` names
overridden overridden
- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent - ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
(class attribute, removed during class creation) (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 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 be provided. It will be checked against the actual order of the enumeration

View File

@ -10,7 +10,11 @@ except ImportError:
from collections import OrderedDict from collections import OrderedDict
__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'unique'] __all__ = [
'EnumMeta',
'Enum', 'IntEnum', 'Flag', 'IntFlag',
'auto', 'unique',
]
def _is_descriptor(obj): def _is_descriptor(obj):
@ -36,7 +40,6 @@ def _is_sunder(name):
name[-2:-1] != '_' and name[-2:-1] != '_' and
len(name) > 2) len(name) > 2)
def _make_class_unpicklable(cls): def _make_class_unpicklable(cls):
"""Make the given class un-picklable.""" """Make the given class un-picklable."""
def _break_on_call_reduce(self, proto): def _break_on_call_reduce(self, proto):
@ -44,6 +47,12 @@ def _make_class_unpicklable(cls):
cls.__reduce_ex__ = _break_on_call_reduce cls.__reduce_ex__ = _break_on_call_reduce
cls.__module__ = '<unknown>' cls.__module__ = '<unknown>'
class auto:
"""
Instances are replaced with an appropriate value in Enum class suites.
"""
pass
class _EnumDict(dict): class _EnumDict(dict):
"""Track enum member order and ensure member names are not reused. """Track enum member order and ensure member names are not reused.
@ -55,6 +64,7 @@ class _EnumDict(dict):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._member_names = [] self._member_names = []
self._last_values = []
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""Changes anything not dundered or not a descriptor. """Changes anything not dundered or not a descriptor.
@ -71,6 +81,8 @@ class _EnumDict(dict):
'_generate_next_value_', '_missing_', '_generate_next_value_', '_missing_',
): ):
raise ValueError('_names_ are reserved for future Enum use') raise ValueError('_names_ are reserved for future Enum use')
if key == '_generate_next_value_':
setattr(self, '_generate_next_value', value)
elif _is_dunder(key): elif _is_dunder(key):
if key == '__order__': if key == '__order__':
key = '_order_' key = '_order_'
@ -81,11 +93,13 @@ class _EnumDict(dict):
if key in self: if key in self:
# enum overwriting a descriptor? # enum overwriting a descriptor?
raise TypeError('%r already defined as: %r' % (key, self[key])) 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._member_names.append(key)
self._last_values.append(value)
super().__setitem__(key, value) super().__setitem__(key, value)
# Dummy value for Enum as EnumMeta explicitly checks for it, but of course # 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. # 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` # 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() names = names.replace(',', ' ').split()
if isinstance(names, (tuple, list)) and isinstance(names[0], str): if isinstance(names, (tuple, list)) and isinstance(names[0], str):
original_names, names = names, [] original_names, names = names, []
last_value = None last_values = []
for count, name in enumerate(original_names): for count, name in enumerate(original_names):
last_value = first_enum._generate_next_value_(name, start, count, last_value) value = first_enum._generate_next_value_(name, start, count, last_values[:])
names.append((name, last_value)) last_values.append(value)
names.append((name, value))
# Here, names is either an iterable of (name, value) or a mapping. # Here, names is either an iterable of (name, value) or a mapping.
for item in names: for item in names:
@ -514,11 +529,15 @@ class Enum(metaclass=EnumMeta):
# still not found -- try _missing_ hook # still not found -- try _missing_ hook
return cls._missing_(value) return cls._missing_(value)
@staticmethod def _generate_next_value_(name, start, count, last_values):
def _generate_next_value_(name, start, count, last_value): for last_value in reversed(last_values):
if not count: try:
return last_value + 1
except TypeError:
pass
else:
return start return start
return last_value + 1
@classmethod @classmethod
def _missing_(cls, value): def _missing_(cls, value):
raise ValueError("%r is not a valid %s" % (value, cls.__name__)) 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): class Flag(Enum):
"""Support for flags""" """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. Generate the next value when not given.
@ -628,7 +647,12 @@ class Flag(Enum):
""" """
if not count: if not count:
return start if start is not None else 1 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) return 2 ** (high_bit+1)
@classmethod @classmethod

View File

@ -3,7 +3,7 @@ import inspect
import pydoc import pydoc
import unittest import unittest
from collections import OrderedDict 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 io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support from test import support
@ -113,6 +113,7 @@ class TestHelpers(unittest.TestCase):
'__', '___', '____', '_____',): '__', '___', '____', '_____',):
self.assertFalse(enum._is_dunder(s)) self.assertFalse(enum._is_dunder(s))
# tests
class TestEnum(unittest.TestCase): class TestEnum(unittest.TestCase):
@ -1578,6 +1579,61 @@ class TestEnum(unittest.TestCase):
self.assertEqual(LabelledList.unprocessed, 1) self.assertEqual(LabelledList.unprocessed, 1)
self.assertEqual(LabelledList(1), LabelledList.unprocessed) 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): 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.CURLY|FlagStooges.MOE)
test_pickle_dump_load(self.assertIs, FlagStooges) test_pickle_dump_load(self.assertIs, FlagStooges)
def test_containment(self): def test_containment(self):
Perm = self.Perm Perm = self.Perm
R, W, X = Perm R, W, X = Perm
@ -1877,6 +1932,24 @@ class TestFlag(unittest.TestCase):
self.assertFalse(W in RX) self.assertFalse(W in RX)
self.assertFalse(X in RW) 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): class TestIntFlag(unittest.TestCase):
"""Tests of the IntFlags.""" """Tests of the IntFlags."""