]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112328: Make EnumDict usable on its own and document it (GH-123669)
authorPetr Viktorin <encukou@gmail.com>
Fri, 20 Dec 2024 19:40:58 +0000 (20:40 +0100)
committerGitHub <noreply@github.com>
Fri, 20 Dec 2024 19:40:58 +0000 (11:40 -0800)
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>
Doc/library/enum.rst
Doc/whatsnew/3.13.rst
Lib/enum.py
Lib/test/test_enum.py
Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst [new file with mode: 0644]

index 51292a11f507c4fc4a3b2e100bf1740e575fc675..8ca949368db4ff1fa01602162b9659d564f6f1a8 100644 (file)
@@ -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
 
 ---------------
 
index a291122aefc2ce1734f99e3799b3eedf4c871331..c8e0f94f4246fbb8e42e9ab974cfa1440c546741 100644 (file)
@@ -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
 ---------
index ccc1da42206474f257fd267af5dd32289f40f982..04443471b40bff2069c25e3b71fcce7de7940a8d 100644 (file)
@@ -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 @@ class EnumDict(dict):
 
         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 @@ class EnumDict(dict):
             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 @@ class EnumType(type):
         # 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:
index b9e13fb8c3585e1ffb57e3fa5408bc56bd0de0ec..8884295b1ab89cd8d38c23154d1718681fda6c10 100644 (file)
@@ -14,7 +14,7 @@ from datetime import date
 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 @@ class TestConvert(unittest.TestCase):
         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 (file)
index 0000000..96da94a
--- /dev/null
@@ -0,0 +1 @@
+:class:`enum.EnumDict` can now be used without resorting to private API.