]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-112328: Make EnumDict usable on its own and document it (GH-123669) (GH...
authorEthan Furman <ethan@stoneleaf.us>
Tue, 24 Dec 2024 18:50:23 +0000 (10:50 -0800)
committerGitHub <noreply@github.com>
Tue, 24 Dec 2024 18:50:23 +0000 (10:50 -0800)
Co-authored-by: Petr Viktorin <pviktori@redhat.com>
Doc/library/enum.rst
Doc/whatsnew/3.13.rst
Lib/enum.py
Lib/test/test_enum.py
Misc/NEWS.d/next/Library/2024-12-20-15-19-38.gh-issue-112328.d9GfLR.rst [new file with mode: 0644]

index 2df9096c45276198a7a0036e27a31c29e0e8833c..24c0cf26496fedfc0a4946767f4e2cd56865763c 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.
@@ -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)
          <KeepFlag.BLUE|16: 20>
 
-.. 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:`|= <object.__ior__>`.
+
+   .. 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
index 3246308efe8afa35265819608c2eb63f3b27eec9..dfc4a98ff82c4b47677901ae26178f0eb3661702 100644 (file)
@@ -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
 ---------
 
index fc765643692db217aff733b0a28cc76a3ff0306b..37f16976bbacde78ef02b8dd87919c9b5e16937b 100644 (file)
@@ -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 @@ 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):
@@ -413,7 +414,7 @@ class EnumDict(dict):
                           '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 @@ 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 e9948de39ed5997fbf597ad57a83c3f9ff3f207d..11e95d5b88b8c930f8678a908ee7f23b38fbf2c4 100644 (file)
@@ -15,7 +15,7 @@ from functools import partial
 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 @@ 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-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 (file)
index 0000000..96da94a
--- /dev/null
@@ -0,0 +1 @@
+:class:`enum.EnumDict` can now be used without resorting to private API.