8000 [3.13] gh-112328: Make EnumDict usable on its own and document it (GH… · python/cpython@875e49f · GitHub
[go: up one dir, main page]

Skip to content

Commit 875e49f

Browse files
ethanfurmanencukou
andauthored
[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>
1 parent a52d663 commit 875e49f

File tree

5 files changed

+72
-8
lines changed

5 files changed

+72
-8
lines changed

Doc/library/enum.rst

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ Module Contents
110110
``KEEP`` which allows for more fine-grained control over how invalid values
111111
are dealt with in an enumeration.
112112

113+
:class:`EnumDict`
114+
115+
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
116+
113117
:class:`auto`
114118

115119
Instances are replaced with an appropriate value for Enum members.
@@ -152,6 +156,7 @@ Module Contents
152156

153157
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
154158
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values``
159+
.. versionadded:: 3.13 ``EnumDict``
155160

156161
---------------
157162

@@ -821,7 +826,27 @@ Data Types
821826
>>> KeepFlag(2**2 + 2**4)
822827
<KeepFlag.BLUE|16: 20>
823828

824-
.. versionadded:: 3.11
829+
.. versionadded:: 3.11
830+
831+
.. class:: EnumDict
832+
833+
*EnumDict* is a subclass of :class:`dict` that is used as the namespace
834+
for defining enum classes (see :ref:`prepare`).
835+
It is exposed to allow subclasses of :class:`EnumType` with advanced
836+
behavior like having multiple values per member.
837+
It should be called with the name of the enum class being created, otherwise
838+
private names and internal classes will not be handled correctly.
839+
840+
Note that only the :class:`~collections.abc.MutableMapping` interface
841+
(:meth:`~object.__setitem__` and :meth:`~dict.update`) is overridden.
842+
It may be possible to bypass the checks using other :class:`!dict`
843+
operations like :meth:`|= <object.__ior__>`.
844+
845+
.. attribute:: EnumDict.member_names
846+
847+
A list of member names.
848+
849+
.. versionadded:: 3.13
825850

826851
---------------
827852

@@ -966,7 +991,6 @@ Utilities and Decorators
966991
Should only be used when the enum members are exported
967992
to the module global namespace (see :class:`re.RegexFlag` for an example).
968993

969-
970994
.. versionadded:: 3.11
971995

972996
.. function:: show_flag_values(value)
@@ -975,6 +999,7 @@ Utilities and Decorators
975999

9761000
.. versionadded:: 3.11
9771001

1002+
9781003
---------------
9791004

9801005
Notes

Doc/whatsnew/3.13.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,13 @@ email
889889
the :cve:`2023-27043` fix.)
890890

891891

892+
enum
893+
----
894+
895+
* :class:`~enum.EnumDict` has been made public to better support subclassing
896+
:class:`~enum.EnumType`.
897+
898+
892899
fractions
893900
---------
894901

Lib/enum.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,13 @@ class EnumDict(dict):
343343
EnumType will use the names found in self._member_names as the
344344
enumeration member names.
345345
"""
346-
def __init__(self):
346+
def __init__(self, cls_name=None):
347347
super().__init__()
348348
self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
349349
self._last_values = []
350350
self._ignore = []
351351
self._auto_called = False
352+
self._cls_name = cls_name
352353

353354
def __setitem__(self, key, value):
354355
"""
@@ -359,7 +360,7 @@ def __setitem__(self, key, value):
359360
360361
Single underscore (sunder) names are reserved.
361362
"""
362-
if _is_private(self._cls_name, key):
363+
if self._cls_name is not None and _is_private(self._cls_name, key):
363364
# do nothing, name will be a normal attribute
364365
pass
365366
elif _is_sunder(key):
@@ -413,7 +414,7 @@ def __setitem__(self, key, value):
413414
'old behavior', FutureWarning, stacklevel=2)
414415
elif _is_descriptor(value):
415416
pass
416-
elif _is_internal_class(self._cls_name, value):
417+
elif self._cls_name is not None and _is_internal_class(self._cls_name, value):
417418
# do nothing, name will be a normal attribute
418419
pass
419420
else:
@@ -485,8 +486,7 @@ def __prepare__(metacls, cls, bases, **kwds):
485486
# check that previous enum members do not exist
486487
metacls._check_for_existing_members_(cls, bases)
487488
# create the namespace dict
488-
enum_dict = EnumDict()
489-
enum_dict._cls_name = cls
489+
enum_dict = EnumDict(cls)
490490
# inherit previous flags and _generate_next_value_ function
491491
member_type, first_enum = metacls._get_mixins_(cls, bases)
492492
if first_enum is not None:

Lib/test/test_enum.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from enum import Enum, EnumMeta, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
1616
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
1717
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
18-
from enum import member, nonmember, _iter_bits_lsb
18+
from enum import member, nonmember, _iter_bits_lsb, EnumDict
1919
from io import StringIO
2020
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
2121
from test import support
@@ -5454,6 +5454,37 @@ def test_convert_repr_and_str(self):
54545454
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
54555455

54565456

5457+
class TestEnumDict(unittest.TestCase):
5458+
def test_enum_dict_in_metaclass(self):
5459+
"""Test that EnumDict is usable as a class namespace"""
5460+
class Meta(type):
5461+
@classmethod
5462+
def __prepare__(metacls, cls, bases, **kwds):
5463+
return EnumDict(cls)
5464+
5465+
class MyClass(metaclass=Meta):
5466+
a = 1
5467+
5468+
with self.assertRaises(TypeError):
5469+
a = 2 # duplicate
5470+
5471+
with self.assertRaises(ValueError):
5472+
_a_sunder_ = 3
5473+
5474+
def test_enum_dict_standalone(self):
5475+
"""Test that EnumDict is usable on its own"""
5476+
enumdict = EnumDict()
5477+
enumdict['a'] = 1
5478+
5479+
with self.assertRaises(TypeError):
5480+
enumdict['a'] = 'other value'
5481+
5482+
# Only MutableMapping interface is overridden for now.
5483+
# If this stops passing, update the documentation.
5484+
enumdict |= {'a': 'other value'}
5485+
self.assertEqual(enumdict['a'], 'other value')
5486+
5487+
54575488
# helpers
54585489

54595490
def enum_dir(cls):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:class:`enum.EnumDict` can now be used without resorting to private API.

0 commit comments

Comments
 (0)
0