gh-112328: Make EnumDict usable on its own and document it (GH-123669)

Co-authored-by: Rafi <rafi.promit@gmail.com>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Ethan Furman <ethan@stoneleaf.us>
This commit is contained in:
Petr Viktorin 2024-12-20 20:40:58 +01:00 committed by GitHub
parent 3879ca0100
commit 2a66dd33df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 60 additions and 16 deletions

View File

@ -110,6 +110,10 @@ Module Contents
``KEEP`` which allows for more fine-grained control over how invalid values ``KEEP`` which allows for more fine-grained control over how invalid values
are dealt with in an enumeration. are dealt with in an enumeration.
:class:`EnumDict`
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
:class:`auto` :class:`auto`
Instances are replaced with an appropriate value for Enum members. Instances are replaced with an appropriate value for Enum members.
@ -149,14 +153,10 @@ Module Contents
Return a list of all power-of-two integers contained in a flag. Return a list of all power-of-two integers contained in a flag.
:class:`EnumDict`
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto`` .. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values`` .. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values``
.. versionadded:: 3.14 ``EnumDict`` .. versionadded:: 3.13 ``EnumDict``
--------------- ---------------
@ -830,13 +830,23 @@ Data Types
.. class:: EnumDict .. class:: EnumDict
*EnumDict* is a subclass of :class:`dict` for use when subclassing :class:`EnumType`. *EnumDict* is a subclass of :class:`dict` that is used as the namespace
for defining enum classes (see :ref:`prepare`).
It is exposed to allow subclasses of :class:`EnumType` with advanced
behavior like having multiple values per member.
It should be called with the name of the enum class being created, otherwise
private names and internal classes will not be handled correctly.
Note that only the :class:`~collections.abc.MutableMapping` interface
(:meth:`~object.__setitem__` and :meth:`~dict.update`) is overridden.
It may be possible to bypass the checks using other :class:`!dict`
operations like :meth:`|= <object.__ior__>`.
.. attribute:: EnumDict.member_names .. attribute:: EnumDict.member_names
Return list of member names. A list of member names.
.. versionadded:: 3.14 .. versionadded:: 3.13
--------------- ---------------

View File

@ -879,11 +879,13 @@ email
(Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve (Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve
the :cve:`2023-27043` fix.) the :cve:`2023-27043` fix.)
enum enum
---- ----
* :class:`~enum.EnumDict` has been made public in :mod:`enum` to better support * :class:`~enum.EnumDict` has been made public to better support subclassing
subclassing :class:`~enum.EnumType`. :class:`~enum.EnumType`.
fractions fractions
--------- ---------

View File

@ -342,12 +342,13 @@ class EnumDict(dict):
EnumType will use the names found in self._member_names as the EnumType will use the names found in self._member_names as the
enumeration member names. enumeration member names.
""" """
def __init__(self): def __init__(self, cls_name=None):
super().__init__() super().__init__()
self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7 self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
self._last_values = [] self._last_values = []
self._ignore = [] self._ignore = []
self._auto_called = False self._auto_called = False
self._cls_name = cls_name
def __setitem__(self, key, value): def __setitem__(self, key, value):
""" """
@ -358,7 +359,7 @@ def __setitem__(self, key, value):
Single underscore (sunder) names are reserved. Single underscore (sunder) names are reserved.
""" """
if _is_private(self._cls_name, key): if self._cls_name is not None and _is_private(self._cls_name, key):
# do nothing, name will be a normal attribute # do nothing, name will be a normal attribute
pass pass
elif _is_sunder(key): elif _is_sunder(key):
@ -406,7 +407,7 @@ def __setitem__(self, key, value):
value = value.value value = value.value
elif _is_descriptor(value): elif _is_descriptor(value):
pass pass
elif _is_internal_class(self._cls_name, value): elif self._cls_name is not None and _is_internal_class(self._cls_name, value):
# do nothing, name will be a normal attribute # do nothing, name will be a normal attribute
pass pass
else: else:
@ -478,8 +479,7 @@ def __prepare__(metacls, cls, bases, **kwds):
# check that previous enum members do not exist # check that previous enum members do not exist
metacls._check_for_existing_members_(cls, bases) metacls._check_for_existing_members_(cls, bases)
# create the namespace dict # create the namespace dict
enum_dict = EnumDict() enum_dict = EnumDict(cls)
enum_dict._cls_name = cls
# inherit previous flags and _generate_next_value_ function # inherit previous flags and _generate_next_value_ function
member_type, first_enum = metacls._get_mixins_(cls, bases) member_type, first_enum = metacls._get_mixins_(cls, bases)
if first_enum is not None: if first_enum is not None:

View File

@ -14,7 +14,7 @@
from enum import Enum, EnumMeta, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto from enum import Enum, EnumMeta, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
from enum import member, nonmember, _iter_bits_lsb from enum import member, nonmember, _iter_bits_lsb, EnumDict
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
@ -5440,6 +5440,37 @@ def test_convert_repr_and_str(self):
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5') self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
class TestEnumDict(unittest.TestCase):
def test_enum_dict_in_metaclass(self):
"""Test that EnumDict is usable as a class namespace"""
class Meta(type):
@classmethod
def __prepare__(metacls, cls, bases, **kwds):
return EnumDict(cls)
class MyClass(metaclass=Meta):
a = 1
with self.assertRaises(TypeError):
a = 2 # duplicate
with self.assertRaises(ValueError):
_a_sunder_ = 3
def test_enum_dict_standalone(self):
"""Test that EnumDict is usable on its own"""
enumdict = EnumDict()
enumdict['a'] = 1
with self.assertRaises(TypeError):
enumdict['a'] = 'other value'
# Only MutableMapping interface is overridden for now.
# If this stops passing, update the documentation.
enumdict |= {'a': 'other value'}
self.assertEqual(enumdict['a'], 'other value')
# helpers # helpers
def enum_dir(cls): def enum_dir(cls):

View File

@ -0,0 +1 @@
:class:`enum.EnumDict` can now be used without resorting to private API.