From c16595e567d51a1773be30c34622620b52be7acf Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Sat, 10 Sep 2016 23:36:59 -0700 Subject: issue23591: add auto() for auto-generating Enum member values --- 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 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 : 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) + [, , ] + +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) + [, , , ] + +.. 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 @@ -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 @@ -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 + + + 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__ = '' +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.""" -- cgit v0.12