While :class:`IntEnum` is part of the :mod:`enum` module, it would be very
simple to implement independently::
- class IntEnum(int, Enum):
+ class IntEnum(int, ReprEnum): # or Enum instead of ReprEnum
pass
This demonstrates how similar derived enumerations can be defined; for example
Some rules:
-1. When subclassing :class:`Enum`, mix-in types must appear before
- :class:`Enum` itself in the sequence of bases, as in the :class:`IntEnum`
+1. When subclassing :class:`Enum`, mix-in types must appear before the
+ :class:`Enum` class itself in the sequence of bases, as in the :class:`IntEnum`
example above.
2. Mix-in types must be subclassable. For example, :class:`bool` and
:class:`range` are not subclassable and will throw an error during Enum
Supported ``_sunder_`` names
""""""""""""""""""""""""""""
-- ``_name_`` -- name of the member
-- ``_value_`` -- value of the member; can be set / modified in ``__new__``
+- :attr:`~Enum._name_` -- name of the member
+- :attr:`~Enum._value_` -- value of the member; can be set in ``__new__``
+- :meth:`~Enum._missing_` -- a lookup function used when a value is not found;
+ may be overridden
+- :attr:`~Enum._ignore_` -- a list of names, either as a :class:`list` or a
+ :class:`str`, that will not be transformed into members, and will be removed
+ from the final class
+- :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for
+ an enum member; may be overridden
+- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing
+ member.
+- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an
+ existing member. See `MultiValueEnum`_ for an example.
-- ``_missing_`` -- a lookup function used when a value is not found; may be
- overridden
-- ``_ignore_`` -- a list of names, either as a :class:`list` or a :class:`str`,
- that will not be transformed into members, and will be removed from the final
- class
-- ``_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
+ .. note::
-.. note::
+ For standard :class:`Enum` classes the next value chosen is the highest
+ value seen incremented by one.
- For standard :class:`Enum` classes the next value chosen is the last value seen
- incremented by one.
+ For :class:`Flag` classes the next value chosen will be the next highest
+ power-of-two.
- For :class:`Flag` classes the next value chosen will be the next highest
- power-of-two, regardless of the last value seen.
+ .. versionchanged:: 3.13
+ Prior versions would use the last seen value instead of the highest value.
.. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
.. versionadded:: 3.7 ``_ignore_``
+.. versionadded:: 3.13 ``_add_alias_``, ``_add_value_alias_``
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
disallowing aliases, the :func:`unique` decorator can be used instead.
+MultiValueEnum
+^^^^^^^^^^^^^^^^^
+
+Supports having more than one value per member::
+
+ >>> class MultiValueEnum(Enum):
+ ... def __new__(cls, value, *values):
+ ... self = object.__new__(cls)
+ ... self._value_ = value
+ ... for v in values:
+ ... self._add_value_alias_(v)
+ ... return self
+ ...
+ >>> class DType(MultiValueEnum):
+ ... float32 = 'f', 8
+ ... double64 = 'd', 9
+ ...
+ >>> DType('f')
+ <DType.float32: 'f'>
+ >>> DType(9)
+ <DType.double64: 'd'>
+
+
Planet
^^^^^^
>>> len(Color)
3
+ .. attribute:: EnumType.__members__
+
+ Returns a mapping of every enum name to its member, including aliases
+
.. method:: EnumType.__reversed__(cls)
Returns each member in *cls* in reverse definition order::
>>> list(reversed(Color))
[<Color.BLUE: 3>, <Color.GREEN: 2>, <Color.RED: 1>]
+ .. method:: EnumType._add_alias_
+
+ Adds a new name as an alias to an existing member. Raises a
+ :exc:`NameError` if the name is already assigned to a different member.
+
+ .. method:: EnumType._add_value_alias_
+
+ Adds a new value as an alias to an existing member. Raises a
+ :exc:`ValueError` if the value is already linked with a different member.
+
.. versionadded:: 3.11
- Before 3.11 ``enum`` used ``EnumMeta`` type, which is kept as an alias.
+ Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias.
.. class:: Enum
>>> PowersOfThree.SECOND.value
9
- .. method:: Enum.__init_subclass__(cls, **kwds)
+ .. method:: Enum.__init_subclass__(cls, \**kwds)
A *classmethod* that is used to further configure subsequent subclasses.
By default, does nothing.
.. method:: __invert__(self):
- Returns all the flags in *type(self)* that are not in self::
+ Returns all the flags in *type(self)* that are not in *self*::
>>> ~white
<Color: 0>
:attr:`~EnumType.__members__` is a read-only ordered mapping of ``member_name``:``member``
items. It is only available on the class.
-:meth:`~object.__new__`, if specified, must create and return the enum members; it is
-also a very good idea to set the member's :attr:`!_value_` appropriately. Once
-all the members are created it is no longer used.
+:meth:`~object.__new__`, if specified, must create and return the enum members;
+it is also a very good idea to set the member's :attr:`!_value_` appropriately.
+Once all the members are created it is no longer used.
Supported ``_sunder_`` names
""""""""""""""""""""""""""""
-- ``_name_`` -- name of the member
-- ``_value_`` -- value of the member; can be set / modified in ``__new__``
-
-- ``_missing_`` -- a lookup function used when a value is not found; may be
- overridden
-- ``_ignore_`` -- a list of names, either as a :class:`list` or a :class:`str`,
- that will not be transformed into members, and will be removed from the final
- class
-- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
- (class attribute, removed during class creation)
-- ``_generate_next_value_`` -- used to get an appropriate value for an enum
- member; may be overridden
+- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing
+ member.
+- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an
+ existing member.
+- :attr:`~Enum._name_` -- name of the member
+- :attr:`~Enum._value_` -- value of the member; can be set in ``__new__``
+- :meth:`~Enum._missing_` -- a lookup function used when a value is not found;
+ may be overridden
+- :attr:`~Enum._ignore_` -- a list of names, either as a :class:`list` or a
+ :class:`str`, that will not be transformed into members, and will be removed
+ from the final class
+- :attr:`~Enum._order_` -- used in Python 2/3 code to ensure member order is
+ consistent (class attribute, removed during class creation)
+- :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for
+ an enum member; may be overridden
.. note::
- For standard :class:`Enum` classes the next value chosen is the last value seen
- incremented by one.
+ For standard :class:`Enum` classes the next value chosen is the highest
+ value seen incremented by one.
For :class:`Flag` classes the next value chosen will be the next highest
- power-of-two, regardless of the last value seen.
+ power-of-two.
.. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
.. versionadded:: 3.7 ``_ignore_``
+.. versionadded:: 3.13 ``_add_alias_``, ``_add_value_alias_``
---------------
__all__ = [
- 'EnumType', 'EnumMeta',
+ 'EnumType', 'EnumMeta', 'EnumDict',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
):
# no other instances found, record this member in _member_names_
enum_class._member_names_.append(member_name)
- # if necessary, get redirect in place and then add it to _member_map_
- found_descriptor = None
- descriptor_type = None
- class_type = None
- for base in enum_class.__mro__[1:]:
- attr = base.__dict__.get(member_name)
- if attr is not None:
- if isinstance(attr, (property, DynamicClassAttribute)):
- found_descriptor = attr
- class_type = base
- descriptor_type = 'enum'
- break
- elif _is_descriptor(attr):
- found_descriptor = attr
- descriptor_type = descriptor_type or 'desc'
- class_type = class_type or base
- continue
- else:
- descriptor_type = 'attr'
- class_type = base
- if found_descriptor:
- redirect = property()
- redirect.member = enum_member
- redirect.__set_name__(enum_class, member_name)
- if descriptor_type in ('enum','desc'):
- # earlier descriptor found; copy fget, fset, fdel to this one.
- redirect.fget = getattr(found_descriptor, 'fget', None)
- redirect._get = getattr(found_descriptor, '__get__', None)
- redirect.fset = getattr(found_descriptor, 'fset', None)
- redirect._set = getattr(found_descriptor, '__set__', None)
- redirect.fdel = getattr(found_descriptor, 'fdel', None)
- redirect._del = getattr(found_descriptor, '__delete__', None)
- redirect._attr_type = descriptor_type
- redirect._cls_type = class_type
- setattr(enum_class, member_name, redirect)
- else:
- setattr(enum_class, member_name, enum_member)
- # now add to _member_map_ (even aliases)
- enum_class._member_map_[member_name] = enum_member
+
+ enum_class._add_member_(member_name, enum_member)
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
except TypeError:
# keep track of the value in a list so containment checks are quick
enum_class._unhashable_values_.append(value)
+ enum_class._unhashable_values_map_.setdefault(member_name, []).append(value)
-class _EnumDict(dict):
+class EnumDict(dict):
"""
Track enum member order and ensure member names are not reused.
"""
def __init__(self):
super().__init__()
- self._member_names = {} # use a dict to keep insertion order
+ self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
self._last_values = []
self._ignore = []
self._auto_called = False
'_order_',
'_generate_next_value_', '_numeric_repr_', '_missing_', '_ignore_',
'_iter_member_', '_iter_member_by_value_', '_iter_member_by_def_',
+ '_add_alias_', '_add_value_alias_',
):
raise ValueError(
'_sunder_ names, such as %r, are reserved for future Enum use'
self._last_values.append(value)
super().__setitem__(key, value)
+ @property
+ def member_names(self):
+ return list(self._member_names)
+
def update(self, members, **more_members):
try:
for name in members.keys():
for name, value in more_members.items():
self[name] = value
+_EnumDict = EnumDict # keep private name for backwards compatibility
+
class EnumType(type):
"""
# check that previous enum members do not exist
metacls._check_for_existing_members_(cls, bases)
# create the namespace dict
- enum_dict = _EnumDict()
+ enum_dict = EnumDict()
enum_dict._cls_name = cls
# inherit previous flags and _generate_next_value_ function
member_type, first_enum = metacls._get_mixins_(cls, bases)
classdict['_member_map_'] = {}
classdict['_value2member_map_'] = {}
classdict['_unhashable_values_'] = []
+ classdict['_unhashable_values_map_'] = {}
classdict['_member_type_'] = member_type
# now set the __repr__ for the value
classdict['_value_repr_'] = metacls._find_data_repr_(cls, bases)
"""
if isinstance(value, cls):
return True
- return value in cls._value2member_map_ or value in cls._unhashable_values_
+ try:
+ return value in cls._value2member_map_
+ except TypeError:
+ return value in cls._unhashable_values_
def __delattr__(cls, attr):
# nicer error message when someone tries to delete an attribute
else:
use_args = True
return __new__, save_new, use_args
-EnumMeta = EnumType
+
+ def _add_member_(cls, name, member):
+ # _value_ structures are not updated
+ if name in cls._member_map_:
+ if cls._member_map_[name] is not member:
+ raise NameError('%r is already bound: %r' % (name, cls._member_map_[name]))
+ return
+ #
+ # if necessary, get redirect in place and then add it to _member_map_
+ found_descriptor = None
+ descriptor_type = None
+ class_type = None
+ for base in cls.__mro__[1:]:
+ attr = base.__dict__.get(name)
+ if attr is not None:
+ if isinstance(attr, (property, DynamicClassAttribute)):
+ found_descriptor = attr
+ class_type = base
+ descriptor_type = 'enum'
+ break
+ elif _is_descriptor(attr):
+ found_descriptor = attr
+ descriptor_type = descriptor_type or 'desc'
+ class_type = class_type or base
+ continue
+ else:
+ descriptor_type = 'attr'
+ class_type = base
+ if found_descriptor:
+ redirect = property()
+ redirect.member = member
+ redirect.__set_name__(cls, name)
+ if descriptor_type in ('enum', 'desc'):
+ # earlier descriptor found; copy fget, fset, fdel to this one.
+ redirect.fget = getattr(found_descriptor, 'fget', None)
+ redirect._get = getattr(found_descriptor, '__get__', None)
+ redirect.fset = getattr(found_descriptor, 'fset', None)
+ redirect._set = getattr(found_descriptor, '__set__', None)
+ redirect.fdel = getattr(found_descriptor, 'fdel', None)
+ redirect._del = getattr(found_descriptor, '__delete__', None)
+ redirect._attr_type = descriptor_type
+ redirect._cls_type = class_type
+ setattr(cls, name, redirect)
+ else:
+ setattr(cls, name, member)
+ # now add to _member_map_ (even aliases)
+ cls._member_map_[name] = member
+ #
+ cls._member_map_[name] = member
+
+EnumMeta = EnumType # keep EnumMeta name for backwards compatibility
class Enum(metaclass=EnumType):
pass
except TypeError:
# not there, now do long search -- O(n) behavior
- for member in cls._member_map_.values():
- if member._value_ == value:
- return member
+ for name, values in cls._unhashable_values_map_.items():
+ if value in values:
+ return cls[name]
# still not found -- verify that members exist, in-case somebody got here mistakenly
# (such as via super when trying to override __new__)
if not cls._member_map_:
def __init__(self, *args, **kwds):
pass
+ def _add_alias_(self, name):
+ self.__class__._add_member_(name, self)
+
+ def _add_value_alias_(self, value):
+ cls = self.__class__
+ try:
+ if value in cls._value2member_map_:
+ if cls._value2member_map_[value] is not self:
+ raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
+ return
+ except TypeError:
+ # unhashable value, do long search
+ for m in cls._member_map_.values():
+ if m._value_ == value:
+ if m is not self:
+ raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
+ return
+ try:
+ # This may fail if value is not hashable. We can't add the value
+ # to the map, and by-value lookups for this value will be
+ # linear.
+ cls._value2member_map_.setdefault(value, self)
+ except TypeError:
+ # keep track of the value in a list so containment checks are quick
+ cls._unhashable_values_.append(value)
+ cls._unhashable_values_map_.setdefault(self.name, []).append(value)
+
@staticmethod
def _generate_next_value_(name, start, count, last_values):
"""
body['_member_names_'] = member_names = []
body['_member_map_'] = member_map = {}
body['_value2member_map_'] = value2member_map = {}
- body['_unhashable_values_'] = []
+ body['_unhashable_values_'] = unhashable_values = []
+ body['_unhashable_values_map_'] = {}
body['_member_type_'] = member_type = etype._member_type_
body['_value_repr_'] = etype._value_repr_
if issubclass(etype, Flag):
for name, value in attrs.items():
if isinstance(value, auto) and auto.value is _auto_null:
value = gnv(name, 1, len(member_names), gnv_last_values)
- if value in value2member_map:
+ if value in value2member_map or value in unhashable_values:
# an alias to an existing member
- member = value2member_map[value]
- redirect = property()
- redirect.member = member
- redirect.__set_name__(enum_class, name)
- setattr(enum_class, name, redirect)
- member_map[name] = member
+ enum_class(value)._add_alias_(name)
else:
# create the member
if use_args:
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
- redirect = property()
- redirect.member = member
- redirect.__set_name__(enum_class, name)
- setattr(enum_class, name, redirect)
- member_map[name] = member
member._sort_order_ = len(member_names)
+ if name not in ('name', 'value'):
+ setattr(enum_class, name, member)
+ member_map[name] = member
+ else:
+ enum_class._add_member_(name, member)
value2member_map[value] = member
if _is_single_bit(value):
# not a multi-bit alias, record in _member_names_ and _flag_mask_
if value.value is _auto_null:
value.value = gnv(name, 1, len(member_names), gnv_last_values)
value = value.value
- if value in value2member_map:
+ try:
+ contained = value in value2member_map
+ except TypeError:
+ contained = value in unhashable_values
+ if contained:
# an alias to an existing member
- member = value2member_map[value]
- redirect = property()
- redirect.member = member
- redirect.__set_name__(enum_class, name)
- setattr(enum_class, name, redirect)
- member_map[name] = member
+ enum_class(value)._add_alias_(name)
else:
# create the member
if use_args:
member.__objclass__ = enum_class
member.__init__(value)
member._sort_order_ = len(member_names)
- redirect = property()
- redirect.member = member
- redirect.__set_name__(enum_class, name)
- setattr(enum_class, name, redirect)
- member_map[name] = member
- value2member_map[value] = member
+ if name not in ('name', 'value'):
+ setattr(enum_class, name, member)
+ member_map[name] = member
+ else:
+ enum_class._add_member_(name, member)
member_names.append(name)
gnv_last_values.append(value)
+ try:
+ # This may fail if value is not hashable. We can't add the value
+ # to the map, and by-value lookups for this value will be
+ # linear.
+ enum_class._value2member_map_.setdefault(value, member)
+ except TypeError:
+ # keep track of the value in a list so containment checks are quick
+ enum_class._unhashable_values_.append(value)
+ enum_class._unhashable_values_map_.setdefault(name, []).append(value)
if '__new__' in body:
enum_class.__new_member__ = enum_class.__new__
enum_class.__new__ = Enum.__new__
self.assertFalse('first' in MainEnum)
val = MainEnum.dupe
self.assertIn(val, MainEnum)
+ self.assertNotIn(float('nan'), MainEnum)
#
class OtherEnum(Enum):
one = auto()
member._value_ = Base(value)
return member
+ def test_extra_member_creation(self):
+ class IDEnumMeta(EnumMeta):
+ def __new__(metacls, cls, bases, classdict, **kwds):
+ # add new entries to classdict
+ for name in classdict.member_names:
+ classdict[f'{name}_DESC'] = f'-{classdict[name]}'
+ return super().__new__(metacls, cls, bases, classdict, **kwds)
+ class IDEnum(StrEnum, metaclass=IDEnumMeta):
+ pass
+ class MyEnum(IDEnum):
+ ID = 'id'
+ NAME = 'name'
+ self.assertEqual(list(MyEnum), [MyEnum.ID, MyEnum.NAME, MyEnum.ID_DESC, MyEnum.NAME_DESC])
+
+ def test_add_alias(self):
+ class mixin:
+ @property
+ def ORG(self):
+ return 'huh'
+ class Color(mixin, Enum):
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+ Color.RED._add_alias_('ROJO')
+ self.assertIs(Color.RED, Color['ROJO'])
+ self.assertIs(Color.RED, Color.ROJO)
+ Color.BLUE._add_alias_('ORG')
+ self.assertIs(Color.BLUE, Color['ORG'])
+ self.assertIs(Color.BLUE, Color.ORG)
+ self.assertEqual(Color.RED.ORG, 'huh')
+ self.assertEqual(Color.GREEN.ORG, 'huh')
+ self.assertEqual(Color.BLUE.ORG, 'huh')
+ self.assertEqual(Color.ORG.ORG, 'huh')
+
+ def test_add_value_alias_after_creation(self):
+ class Color(Enum):
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+ Color.RED._add_value_alias_(5)
+ self.assertIs(Color.RED, Color(5))
+
+ def test_add_value_alias_during_creation(self):
+ class Types(Enum):
+ Unknown = 0,
+ Source = 1, 'src'
+ NetList = 2, 'nl'
+ def __new__(cls, int_value, *value_aliases):
+ member = object.__new__(cls)
+ member._value_ = int_value
+ for alias in value_aliases:
+ member._add_value_alias_(alias)
+ return member
+ self.assertIs(Types(0), Types.Unknown)
+ self.assertIs(Types(1), Types.Source)
+ self.assertIs(Types('src'), Types.Source)
+ self.assertIs(Types(2), Types.NetList)
+ self.assertIs(Types('nl'), Types.NetList)
+
class TestOrder(unittest.TestCase):
"test usage of the `_order_` attribute"
@bltns.property
def zeroth(self):
return 'zeroed %s' % self.name
- self.assertTrue(_test_simple_enum(CheckedColor, SimpleColor) is None)
+ _test_simple_enum(CheckedColor, SimpleColor)
SimpleColor.MAGENTA._value_ = 9
self.assertRaisesRegex(
TypeError, "enum mismatch",
_test_simple_enum, CheckedColor, SimpleColor,
)
+ #
+ #
class CheckedMissing(IntFlag, boundary=KEEP):
SIXTY_FOUR = 64
ONE_TWENTY_EIGHT = 128
ALL = 2048 + 128 + 64 + 12
M = Missing
self.assertEqual(list(CheckedMissing), [M.SIXTY_FOUR, M.ONE_TWENTY_EIGHT, M.TWENTY_FORTY_EIGHT])
- #
_test_simple_enum(CheckedMissing, Missing)
+ #
+ #
+ class CheckedUnhashable(Enum):
+ ONE = dict()
+ TWO = set()
+ name = 'python'
+ self.assertIn(dict(), CheckedUnhashable)
+ self.assertIn('python', CheckedUnhashable)
+ self.assertEqual(CheckedUnhashable.name.value, 'python')
+ self.assertEqual(CheckedUnhashable.name.name, 'name')
+ #
+ @_simple_enum()
+ class Unhashable:
+ ONE = dict()
+ TWO = set()
+ name = 'python'
+ self.assertIn(dict(), Unhashable)
+ self.assertIn('python', Unhashable)
+ self.assertEqual(Unhashable.name.value, 'python')
+ self.assertEqual(Unhashable.name.name, 'name')
+ _test_simple_enum(Unhashable, Unhashable)
class MiscTestCase(unittest.TestCase):