mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	issue23591: add auto() for auto-generating Enum member values
This commit is contained in:
		
							parent
							
								
									944368e1cc
								
							
						
					
					
						commit
						c16595e567
					
				
					 3 changed files with 195 additions and 31 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										50
									
								
								Lib/enum.py
									
										
									
									
									
								
							
							
						
						
									
										50
									
								
								Lib/enum.py
									
										
									
									
									
								
							|  | @ -10,7 +10,11 @@ | |||
|     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 _break_on_call_reduce(self, proto): | |||
|     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 @@ def __setitem__(self, key, value): | |||
|                     '_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 @@ def __setitem__(self, key, value): | |||
|             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 @@ def _create_(cls, class_name, names=None, *, module=None, qualname=None, type=No | |||
|             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 @@ def __new__(cls, value): | |||
|         # 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 @@ def _generate_next_value_(name, start, count, last_value): | |||
|         """ | ||||
|         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 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 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 @@ def test_is_dunder(self): | |||
|                 '__', '___', '____', '_____',): | ||||
|             self.assertFalse(enum._is_dunder(s)) | ||||
| 
 | ||||
| # tests | ||||
| 
 | ||||
| class TestEnum(unittest.TestCase): | ||||
| 
 | ||||
|  | @ -1578,6 +1579,61 @@ class LabelledList(LabelledIntEnum): | |||
|         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 @@ def test_pickle(self): | |||
|         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 @@ def test_containment(self): | |||
|         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.""" | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Ethan Furman
						Ethan Furman