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

Co-authored-by: Petr Viktorin <pviktori@redhat.com>
This commit is contained in:
Ethan Furman 2024-12-24 10:50:23 -08:00 committed by GitHub
parent a52d663d0c
commit 875e49fb63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 8 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.
@ -152,6 +156,7 @@ Module Contents
.. 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.13 ``EnumDict``
--------------- ---------------
@ -823,6 +828,26 @@ Data Types
.. versionadded:: 3.11 .. versionadded:: 3.11
.. class:: EnumDict
*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
A list of member names.
.. versionadded:: 3.13
--------------- ---------------
Supported ``__dunder__`` names Supported ``__dunder__`` names
@ -966,7 +991,6 @@ Utilities and Decorators
Should only be used when the enum members are exported Should only be used when the enum members are exported
to the module global namespace (see :class:`re.RegexFlag` for an example). to the module global namespace (see :class:`re.RegexFlag` for an example).
.. versionadded:: 3.11 .. versionadded:: 3.11
.. function:: show_flag_values(value) .. function:: show_flag_values(value)
@ -975,6 +999,7 @@ Utilities and Decorators
.. versionadded:: 3.11 .. versionadded:: 3.11
--------------- ---------------
Notes Notes

View file

@ -889,6 +889,13 @@ email
the :cve:`2023-27043` fix.) the :cve:`2023-27043` fix.)
enum
----
* :class:`~enum.EnumDict` has been made public to better support subclassing
:class:`~enum.EnumType`.
fractions fractions
--------- ---------

View file

@ -343,12 +343,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):
""" """
@ -359,7 +360,7 @@ class EnumDict(dict):
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):
@ -413,7 +414,7 @@ class EnumDict(dict):
'old behavior', FutureWarning, stacklevel=2) 'old behavior', FutureWarning, stacklevel=2)
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:
@ -485,8 +486,7 @@ class EnumType(type):
# 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

@ -15,7 +15,7 @@ from functools import partial
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
@ -5454,6 +5454,37 @@ class TestConvert(unittest.TestCase):
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.