Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions Doc/howto/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -868,16 +868,16 @@ Others
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
a :class:`FloatEnum` that mixes in :class:`float` instead of :class:`int`.

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
Expand Down Expand Up @@ -961,30 +961,36 @@ 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 by the `Functional API`_ and by
:class:`auto` 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 :class:`Flag` classes the next value chosen will be the next highest
power-of-two, regardless of the last value seen.
- :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
Comment thread
sobolevn marked this conversation as resolved.
Resolved
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop Python2 part.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping for now -- I'll probably change _order_ to be definitive so Enum still works on python implementations without ordered dictionaries.

consistent (class attribute, removed during class creation)
Comment thread
ethanfurman marked this conversation as resolved.
Resolved
- :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 highest
value seen incremented by one.

For :class:`Flag` classes the next value chosen will be the next highest
power-of-two.

.. 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
Expand Down
60 changes: 39 additions & 21 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,30 @@ Data Types
>>> len(Color)
3

.. attribute: EnumType.__members__
Comment thread
ethanfurman marked this conversation as resolved.
Resolved

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_
Comment thread
ethanfurman marked this conversation as resolved.
Resolved

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_
Comment thread
ethanfurman marked this conversation as resolved.
Resolved

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
Expand Down Expand Up @@ -323,7 +337,7 @@ Data Types
>>> 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.
Expand Down Expand Up @@ -549,7 +563,7 @@ Data Types

.. 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>
Expand Down Expand Up @@ -769,37 +783,41 @@ Supported ``__dunder__`` names
: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
Comment thread
sobolevn marked this conversation as resolved.
Resolved
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_``

---------------

Expand Down
132 changes: 87 additions & 45 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


__all__ = [
'EnumType', 'EnumMeta',
'EnumType', 'EnumMeta', 'EnumDict',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
Expand Down Expand Up @@ -313,45 +313,8 @@ def __set_name__(self, enum_class, member_name):
):
# 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_(enum_member, member_name)
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
Expand All @@ -362,7 +325,7 @@ def __set_name__(self, enum_class, member_name):
enum_class._unhashable_values_.append(value)


class _EnumDict(dict):
class EnumDict(dict):
"""
Track enum member order and ensure member names are not reused.

Expand All @@ -371,7 +334,7 @@ class _EnumDict(dict):
"""
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 (?)
Comment thread
sobolevn marked this conversation as resolved.
Resolved
Comment thread
ethanfurman marked this conversation as resolved.
Resolved
self._last_values = []
self._ignore = []
self._auto_called = False
Expand Down Expand Up @@ -468,6 +431,10 @@ def __setitem__(self, key, value):
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():
Expand All @@ -478,6 +445,8 @@ def update(self, members, **more_members):
for name, value in more_members.items():
self[name] = value

_EnumDict = EnumDict # keep private name for backwards compatibility


class EnumType(type):
"""
Expand All @@ -489,7 +458,7 @@ def __prepare__(metacls, cls, bases, **kwds):
# 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)
Expand Down Expand Up @@ -1050,7 +1019,80 @@ def _find_new_(mcls, classdict, member_type, first_enum):
else:
use_args = True
return __new__, save_new, use_args
EnumMeta = EnumType

def _add_alias_(cls, member, name):
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'):
Comment thread
ethanfurman marked this conversation as resolved.
Resolved
# 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
_add_member_ = _add_alias_ # use _add_member_ internally

def _add_value_alias_(cls, member, value):
try:
if value in cls._value2member_map_:
if cls._value2member_map_[value] is not member:
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 member:
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, member)
except TypeError:
# keep track of the value in a list so containment checks are quick
cls._unhashable_values_.append(value)


EnumMeta = EnumType # keep EnumMeta name for backwards compatibility


class Enum(metaclass=EnumType):
Expand Down Expand Up @@ -1117,7 +1159,7 @@ def __new__(cls, value):
except TypeError:
# not there, now do long search -- O(n) behavior
for member in cls._member_map_.values():
if member._value_ == value:
if member._value_ is value or member._value_ == value:
return member
# still not found -- verify that members exist, in-case somebody got here mistakenly
# (such as via super when trying to override __new__)
Expand Down
Loading