https://github.com/python/cpython/commit/2a66dd33dfc0b845042da9bb54aaa4e890733f54
commit: 2a66dd33dfc0b845042da9bb54aaa4e890733f54
branch: main
author: Petr Viktorin <[email protected]>
committer: ethanfurman <[email protected]>
date: 2024-12-20T11:40:58-08:00
summary:

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

Co-authored-by: Rafi <[email protected]>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) 
<[email protected]>
Co-authored-by: Ethan Furman <[email protected]>

files:
A Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst
M Doc/library/enum.rst
M Doc/whatsnew/3.13.rst
M Lib/enum.py
M Lib/test/test_enum.py

diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index 51292a11f507c4..8ca949368db4ff 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.
@@ -149,14 +153,10 @@ Module Contents
 
       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.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
 
-   *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
 
-      Return list of member names.
+      A list of member names.
 
-   .. versionadded:: 3.14
+   .. versionadded:: 3.13
 
 ---------------
 
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index a291122aefc2ce..c8e0f94f4246fb 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -879,11 +879,13 @@ email
   (Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve
   the :cve:`2023-27043` fix.)
 
+
 enum
 ----
 
-* :class:`~enum.EnumDict` has been made public in :mod:`enum` to better support
-  subclassing :class:`~enum.EnumType`.
+* :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 ccc1da42206474..04443471b40bff 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -342,12 +342,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):
         """
@@ -358,7 +359,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):
@@ -406,7 +407,7 @@ def __setitem__(self, key, value):
             value = value.value
         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:
@@ -478,8 +479,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 b9e13fb8c3585e..8884295b1ab89c 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -14,7 +14,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
@@ -5440,6 +5440,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-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst 
b/Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst
new file mode 100644
index 00000000000000..96da94a9f211af
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst
@@ -0,0 +1 @@
+:class:`enum.EnumDict` can now be used without resorting to private API.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to