From d25111e9c0eed659d563b538bba376620d74b309 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 20 Dec 2024 15:19:59 -0800 Subject: [PATCH] EnumDict can now be used without resorting to private API. --- Doc/library/enum.rst | 29 ++++++++++++++-- Doc/whatsnew/3.13.rst | 7 ++++ Lib/enum.py | 10 +++--- Lib/test/test_enum.py | 33 ++++++++++++++++++- ...-12-20-15-19-38.gh-issue-112328.d9GfLR.rst | 1 + 5 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-12-20-15-19-38.gh-issue-112328.d9GfLR.rst diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 2df9096c452761..24c0cf26496fed 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -110,6 +110,10 @@ Module Contents ``KEEP`` which allows for more fine-grained control over how invalid values are dealt with in an enumeration. + :class:`EnumDict` + + A subclass of :class:`dict` for use when subclassing :class:`EnumType`. + :class:`auto` Instances are replaced with an appropriate value for Enum members. @@ -152,6 +156,7 @@ Module Contents .. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto`` .. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values`` +.. versionadded:: 3.13 ``EnumDict`` --------------- @@ -821,7 +826,27 @@ Data Types >>> KeepFlag(2**2 + 2**4) -.. 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:`|= `. + + .. attribute:: EnumDict.member_names + + A list of member names. + + .. versionadded:: 3.13 --------------- @@ -966,7 +991,6 @@ Utilities and Decorators Should only be used when the enum members are exported to the module global namespace (see :class:`re.RegexFlag` for an example). - .. versionadded:: 3.11 .. function:: show_flag_values(value) @@ -975,6 +999,7 @@ Utilities and Decorators .. versionadded:: 3.11 + --------------- Notes diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 237b1d5f642676..4db3e2ac856837 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -889,6 +889,13 @@ email the :cve:`2023-27043` fix.) +enum +---- + +* :class:`~enum.EnumDict` has been made public to better support subclassing + :class:`~enum.EnumType`. + + fractions --------- diff --git a/Lib/enum.py b/Lib/enum.py index fc765643692db2..37f16976bbacde 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -343,12 +343,13 @@ class EnumDict(dict): EnumType will use the names found in self._member_names as the enumeration member names. """ - def __init__(self): + def __init__(self, cls_name=None): super().__init__() 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 + self._cls_name = cls_name def __setitem__(self, key, value): """ @@ -359,7 +360,7 @@ def __setitem__(self, key, value): 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 pass elif _is_sunder(key): @@ -413,7 +414,7 @@ def __setitem__(self, key, value): 'old behavior', FutureWarning, stacklevel=2) elif _is_descriptor(value): 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 pass else: @@ -485,8 +486,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._cls_name = cls + enum_dict = EnumDict(cls) # inherit previous flags and _generate_next_value_ function member_type, first_enum = metacls._get_mixins_(cls, bases) if first_enum is not None: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index e9948de39ed599..11e95d5b88b8c9 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -15,7 +15,7 @@ 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 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 pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -5454,6 +5454,37 @@ def test_convert_repr_and_str(self): 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 def enum_dir(cls): diff --git a/Misc/NEWS.d/next/Library/2024-12-20-15-19-38.gh-issue-112328.d9GfLR.rst b/Misc/NEWS.d/next/Library/2024-12-20-15-19-38.gh-issue-112328.d9GfLR.rst new file mode 100644 index 00000000000000..96da94a9f211af --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-20-15-19-38.gh-issue-112328.d9GfLR.rst @@ -0,0 +1 @@ +:class:`enum.EnumDict` can now be used without resorting to private API.