issue23591: add auto() for auto-generating Enum member values
This commit is contained in:
parent
944368e1cc
commit
c16595e567
|
@ -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
|
||||||
|
|
50
Lib/enum.py
50
Lib/enum.py
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue