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
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

View File

@ -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

View File

@ -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."""