Skip to content

Commit 2a66dd3

Browse files
encukouu2rafiwebknjazethanfurman
authored
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>
1 parent 3879ca0 commit 2a66dd3

File tree

5 files changed

+60
-16
lines changed

5 files changed

+60
-16
lines changed

Doc/library/enum.rst

+18-8
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.
@@ -149,14 +153,10 @@ Module Contents
149153

150154
Return a list of all power-of-two integers contained in a flag.
151155

152-
:class:`EnumDict`
153-
154-
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
155-
156156

157157
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
158158
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values``
159-
.. versionadded:: 3.14 ``EnumDict``
159+
.. versionadded:: 3.13 ``EnumDict``
160160

161161
---------------
162162

@@ -830,13 +830,23 @@ Data Types
830830

831831
.. class:: EnumDict
832832

833-
*EnumDict* is a subclass of :class:`dict` for use when subclassing :class:`EnumType`.
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__>`.
834844

835845
.. attribute:: EnumDict.member_names
836846

837-
Return list of member names.
847+
A list of member names.
838848

839-
.. versionadded:: 3.14
849+
.. versionadded:: 3.13
840850

841851
---------------
842852

Doc/whatsnew/3.13.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -879,11 +879,13 @@ email
879879
(Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve
880880
the :cve:`2023-27043` fix.)
881881

882+
882883
enum
883884
----
884885

885-
* :class:`~enum.EnumDict` has been made public in :mod:`enum` to better support
886-
subclassing :class:`~enum.EnumType`.
886+
* :class:`~enum.EnumDict` has been made public to better support subclassing
887+
:class:`~enum.EnumType`.
888+
887889

888890
fractions
889891
---------

Lib/enum.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,13 @@ class EnumDict(dict):
342342
EnumType will use the names found in self._member_names as the
343343
enumeration member names.
344344
"""
345-
def __init__(self):
345+
def __init__(self, cls_name=None):
346346
super().__init__()
347347
self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
348348
self._last_values = []
349349
self._ignore = []
350350
self._auto_called = False
351+
self._cls_name = cls_name
351352

352353
def __setitem__(self, key, value):
353354
"""
@@ -358,7 +359,7 @@ def __setitem__(self, key, value):
358359
359360
Single underscore (sunder) names are reserved.
360361
"""
361-
if _is_private(self._cls_name, key):
362+
if self._cls_name is not None and _is_private(self._cls_name, key):
362363
# do nothing, name will be a normal attribute
363364
pass
364365
elif _is_sunder(key):
@@ -406,7 +407,7 @@ def __setitem__(self, key, value):
406407
value = value.value
407408
elif _is_descriptor(value):
408409
pass
409-
elif _is_internal_class(self._cls_name, value):
410+
elif self._cls_name is not None and _is_internal_class(self._cls_name, value):
410411
# do nothing, name will be a normal attribute
411412
pass
412413
else:
@@ -478,8 +479,7 @@ def __prepare__(metacls, cls, bases, **kwds):
478479
# check that previous enum members do not exist
479480
metacls._check_for_existing_members_(cls, bases)
480481
# create the namespace dict
481-
enum_dict = EnumDict()
482-
enum_dict._cls_name = cls
482+
enum_dict = EnumDict(cls)
483483
# inherit previous flags and _generate_next_value_ function
484484
member_type, first_enum = metacls._get_mixins_(cls, bases)
485485
if first_enum is not None:

Lib/test/test_enum.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from enum import Enum, EnumMeta, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
1515
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
1616
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
17-
from enum import member, nonmember, _iter_bits_lsb
17+
from enum import member, nonmember, _iter_bits_lsb, EnumDict
1818
from io import StringIO
1919
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
2020
from test import support
@@ -5440,6 +5440,37 @@ def test_convert_repr_and_str(self):
54405440
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
54415441

54425442

5443+
class TestEnumDict(unittest.TestCase):
5444+
def test_enum_dict_in_metaclass(self):
5445+
"""Test that EnumDict is usable as a class namespace"""
5446+
class Meta(type):
5447+
@classmethod
5448+
def __prepare__(metacls, cls, bases, **kwds):
5449+
return EnumDict(cls)
5450+
5451+
class MyClass(metaclass=Meta):
5452+
a = 1
5453+
5454+
with self.assertRaises(TypeError):
5455+
a = 2 # duplicate
5456+
5457+
with self.assertRaises(ValueError):
5458+
_a_sunder_ = 3
5459+
5460+
def test_enum_dict_standalone(self):
5461+
"""Test that EnumDict is usable on its own"""
5462+
enumdict = EnumDict()
5463+
enumdict['a'] = 1
5464+
5465+
with self.assertRaises(TypeError):
5466+
enumdict['a'] = 'other value'
5467+
5468+
# Only MutableMapping interface is overridden for now.
5469+
# If this stops passing, update the documentation.
5470+
enumdict |= {'a': 'other value'}
5471+
self.assertEqual(enumdict['a'], 'other value')
5472+
5473+
54435474
# helpers
54445475

54455476
def enum_dir(cls):
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)