From d00adac946aa136019f7c7ee68c0a230c4179111 Mon Sep 17 00:00:00 2001 From: Rafi Date: Sun, 14 Jul 2024 10:17:19 +0200 Subject: [PATCH 01/10] Document EnumDict in docs and release notes --- Doc/library/enum.rst | 20 +++++++++++++++++++- Doc/whatsnew/3.13.rst | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 8b3f397ea862f4..8192d270208381 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -64,6 +64,10 @@ are not normal Python classes. See Module Contents --------------- + :class:`EnumDict` + + An enum class :class:`dict` that tracks order and enforces unique member names. + :class:`EnumType` The ``type`` for Enum and its subclasses. @@ -159,7 +163,21 @@ Data Types ---------- -.. class:: EnumType +.. class:: EnumDict + + *EnumDict* is used by *EnumType* to keep track of the enum member orders and prevent reusing the member names. + + .. attribute:: EnumDict.member_names + + Return list of member names. + + .. method:: EnumDict.__setitem__(self, key, value) + + Set any item as an enum member that is not dundered and not a descriptor. + + .. method:: EnumDict.update(self, members, **more_members) + + Update the dictionary from the given iterable or dictionary members and more_members. *EnumType* is the :term:`metaclass` for *enum* enumerations. It is possible to subclass *EnumType* -- see :ref:`Subclassing EnumType ` diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 4a9a0b77d068b3..97e64abc9c8beb 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -742,6 +742,11 @@ 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`. + fractions --------- From 3bff0b954adddedc766cf8309d06b62e7d9e139f Mon Sep 17 00:00:00 2001 From: Rafi Date: Sun, 14 Jul 2024 10:26:43 +0200 Subject: [PATCH 02/10] Restore deleted EnumType --- Doc/library/enum.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 8192d270208381..d6f14745bb5fb2 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -183,6 +183,8 @@ Data Types to subclass *EnumType* -- see :ref:`Subclassing EnumType ` for details. +.. class:: EnumType + ``EnumType`` is responsible for setting the correct :meth:`!__repr__`, :meth:`!__str__`, :meth:`!__format__`, and :meth:`!__reduce__` methods on the final *enum*, as well as creating the enum members, properly handling From 11e2fd194b6c74f0c0b2f7c7d1babb52a488a33a Mon Sep 17 00:00:00 2001 From: Md Rokibul Islam Date: Tue, 16 Jul 2024 23:37:19 +0200 Subject: [PATCH 03/10] Update Doc/library/enum.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- Doc/library/enum.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index d6f14745bb5fb2..2d1d8c0cf1f680 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -179,12 +179,12 @@ Data Types Update the dictionary from the given iterable or dictionary members and more_members. +.. class:: EnumType + *EnumType* is the :term:`metaclass` for *enum* enumerations. It is possible to subclass *EnumType* -- see :ref:`Subclassing EnumType ` for details. -.. class:: EnumType - ``EnumType`` is responsible for setting the correct :meth:`!__repr__`, :meth:`!__str__`, :meth:`!__format__`, and :meth:`!__reduce__` methods on the final *enum*, as well as creating the enum members, properly handling From e8c368ef9bf2ae1ac2eda0a62650ef28d00fd50c Mon Sep 17 00:00:00 2001 From: Rafi Date: Sun, 28 Jul 2024 22:08:54 +0200 Subject: [PATCH 04/10] Update description --- Doc/library/enum.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 2d1d8c0cf1f680..426d0b86ff32bd 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -66,7 +66,7 @@ Module Contents :class:`EnumDict` - An enum class :class:`dict` that tracks order and enforces unique member names. + A subclass of :class:`dict` that tracks order and enforces unique member names. :class:`EnumType` @@ -165,7 +165,7 @@ Data Types .. class:: EnumDict - *EnumDict* is used by *EnumType* to keep track of the enum member orders and prevent reusing the member names. + *EnumDict* is a subclass of :class:`dict` that keeps track of the order of enum members and prevents reusing member names. Use *EnumDict* when member names must be unique and their order needs to be preserved. .. attribute:: EnumDict.member_names From 05ea9bd80d2b00008ea8e9565ce2063b58951883 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 4 Sep 2024 13:26:40 +0200 Subject: [PATCH 05/10] Put EnumDict docs on the buttom to de-emphasize the class --- Doc/library/enum.rst | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 426d0b86ff32bd..02afa6bbeeb105 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -64,10 +64,6 @@ are not normal Python classes. See Module Contents --------------- - :class:`EnumDict` - - A subclass of :class:`dict` that tracks order and enforces unique member names. - :class:`EnumType` The ``type`` for Enum and its subclasses. @@ -114,6 +110,11 @@ 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` that tracks order and enforces unique + member names. + :class:`auto` Instances are replaced with an appropriate value for Enum members. @@ -163,22 +164,6 @@ Data Types ---------- -.. class:: EnumDict - - *EnumDict* is a subclass of :class:`dict` that keeps track of the order of enum members and prevents reusing member names. Use *EnumDict* when member names must be unique and their order needs to be preserved. - - .. attribute:: EnumDict.member_names - - Return list of member names. - - .. method:: EnumDict.__setitem__(self, key, value) - - Set any item as an enum member that is not dundered and not a descriptor. - - .. method:: EnumDict.update(self, members, **more_members) - - Update the dictionary from the given iterable or dictionary members and more_members. - .. class:: EnumType *EnumType* is the :term:`metaclass` for *enum* enumerations. It is possible @@ -841,6 +826,23 @@ Data Types .. versionadded:: 3.11 +.. class:: EnumDict + + *EnumDict* is a subclass of :class:`dict` that keeps track of the order of enum members and prevents reusing member names. Use *EnumDict* when member names must be unique and their order needs to be preserved. + + .. attribute:: EnumDict.member_names + + Return list of member names. + + .. method:: EnumDict.__setitem__(self, key, value) + + Set any item as an enum member that is not dundered and not a descriptor. + + .. method:: EnumDict.update(self, members, **more_members) + + Update the dictionary from the given iterable or dictionary members and more_members. + + --------------- Supported ``__dunder__`` names From 3e88296783966f5fc40ee584a6713665cc2a2462 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 4 Sep 2024 13:27:13 +0200 Subject: [PATCH 06/10] Add a versionadded entry, and properly indent a previous one --- Doc/library/enum.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 02afa6bbeeb105..ff95a48423d029 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -824,7 +824,7 @@ Data Types >>> KeepFlag(2**2 + 2**4) -.. versionadded:: 3.11 + .. versionadded:: 3.11 .. class:: EnumDict @@ -842,6 +842,7 @@ Data Types Update the dictionary from the given iterable or dictionary members and more_members. + .. versionadded:: 3.13 --------------- From 162ebf01865307b27d9fd2f20eb79eb24d5e47bb Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 4 Sep 2024 14:11:56 +0200 Subject: [PATCH 07/10] Reword the documentation --- Doc/library/enum.rst | 22 ++++++++++--------- Doc/whatsnew/3.13.rst | 4 +++- ...-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst | 2 ++ 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index ff95a48423d029..4d07837bf3574b 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -828,19 +828,21 @@ Data Types .. class:: EnumDict - *EnumDict* is a subclass of :class:`dict` that keeps track of the order of enum members and prevents reusing member names. Use *EnumDict* when member names must be unique and their order needs to be preserved. + *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 prevents reusing member names, with special behavior for names that + start with an underscore. + + 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:`|= `. .. attribute:: EnumDict.member_names - Return list of member names. - - .. method:: EnumDict.__setitem__(self, key, value) - - Set any item as an enum member that is not dundered and not a descriptor. - - .. method:: EnumDict.update(self, members, **more_members) - - Update the dictionary from the given iterable or dictionary members and more_members. + A list of member names. .. versionadded:: 3.13 diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 97e64abc9c8beb..197fac9c897190 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -745,7 +745,9 @@ email enum ---- -* :class:`~enum.EnumDict` has been made public in :mod:`enum`. +* :class:`enum.EnumDict` has been made public, in order to allow subclasses + of :class:`~enum.EnumType` with advanced behavior like having multiple values + per member. fractions --------- 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..5a6ccfe85e3a3c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst @@ -0,0 +1,2 @@ +:class:`enum.EnumDict` can now be used on its own, without resorting to +private API. From 5d9c81b34ad2415611bbb0221b00c4a096bbc93c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 4 Sep 2024 14:12:28 +0200 Subject: [PATCH 08/10] Allow usage of EnumDict without setting a private attr. Add tests. --- Lib/enum.py | 5 +++-- Lib/test/test_enum.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index c36fc75a24a239..5576c46840050c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -346,6 +346,7 @@ def __init__(self): self._last_values = [] self._ignore = [] self._auto_called = False + self._cls_name = None def __setitem__(self, key, value): """ @@ -356,7 +357,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): @@ -404,7 +405,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: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 99fd16ba361e6f..2f3711390f7dcc 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 @@ -5414,6 +5414,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() + + 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 starts passing, update the documentation. + enumdict |= {'a': 'other value'} + self.assertEqual(enumdict['a'], 'other value') + + # helpers def enum_dir(cls): From 694d8f73997c469a5c32453b8dd73df7e87c7f4b Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 19 Dec 2024 16:23:47 -0800 Subject: [PATCH 09/10] updates --- Doc/library/enum.rst | 9 ++------- Doc/whatsnew/3.13.rst | 10 ++-------- Lib/enum.py | 11 +++++------ Lib/test/test_enum.py | 6 +++--- .../2024-09-04-14-13-14.gh-issue-121720.z9hhXQ.rst | 3 +-- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index c78be60277b214..1cc9b507201262 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -112,8 +112,7 @@ Module Contents :class:`EnumDict` - A subclass of :class:`dict` that tracks order and enforces unique - member names. + A subclass of :class:`dict` for use when subclassing :class:`EnumType`. :class:`auto` @@ -154,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`` --------------- diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 7935f25504f38a..c8e0f94f4246fb 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -879,18 +879,12 @@ 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`. enum ---- -* :class:`enum.EnumDict` has been made public, in order to allow subclasses - of :class:`~enum.EnumType` with advanced behavior like having multiple values - per member. +* :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 17d83c732441d1..f6ce29533e07b3 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -342,13 +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): 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 = None + self._cls_name = cls_name def __setitem__(self, key, value): """ @@ -359,7 +359,7 @@ def __setitem__(self, key, value): Single underscore (sunder) names are reserved. """ - if self._cls_name is not None and _is_private(self._cls_name, key): + if _is_private(self._cls_name, key): # do nothing, name will be a normal attribute pass elif _is_sunder(key): @@ -407,7 +407,7 @@ def __setitem__(self, key, value): value = value.value elif _is_descriptor(value): pass - elif self._cls_name is not None and _is_internal_class(self._cls_name, value): + elif _is_internal_class(self._cls_name, value): # do nothing, name will be a normal attribute pass else: @@ -479,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 22e397f333bee6..fa12b8513b3250 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -5446,7 +5446,7 @@ def test_enum_dict_in_metaclass(self): class Meta(type): @classmethod def __prepare__(metacls, cls, bases, **kwds): - return EnumDict() + return EnumDict(cls) class MyClass(metaclass=Meta): a = 1 @@ -5459,14 +5459,14 @@ class MyClass(metaclass=Meta): def test_enum_dict_standalone(self): """Test that EnumDict is usable on its own""" - enumdict = EnumDict() + enumdict = EnumDict('test') enumdict['a'] = 1 with self.assertRaises(TypeError): enumdict['a'] = 'other value' # Only MutableMapping interface is overridden for now. - # If this starts passing, update the documentation. + # If this stops passing, update the documentation. enumdict |= {'a': 'other value'} self.assertEqual(enumdict['a'], 'other value') 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 index 5a6ccfe85e3a3c..96da94a9f211af 100644 --- 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 @@ -1,2 +1 @@ -:class:`enum.EnumDict` can now be used on its own, without resorting to -private API. +:class:`enum.EnumDict` can now be used without resorting to private API. From 9d6908c157c6fa25e5b93a960e14f13f3570f5fd Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 19 Dec 2024 16:46:57 -0800 Subject: [PATCH 10/10] more fixes --- Doc/library/enum.rst | 4 ++-- Lib/enum.py | 6 +++--- Lib/test/test_enum.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 1cc9b507201262..8ca949368db4ff 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -834,8 +834,8 @@ Data Types 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 prevents reusing member names, with special behavior for names that - start with an underscore. + 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. diff --git a/Lib/enum.py b/Lib/enum.py index f6ce29533e07b3..04443471b40bff 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -342,7 +342,7 @@ class EnumDict(dict): EnumType will use the names found in self._member_names as the enumeration member names. """ - def __init__(self, cls_name): + 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 = [] @@ -359,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): @@ -407,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: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index fa12b8513b3250..8884295b1ab89c 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -5459,7 +5459,7 @@ class MyClass(metaclass=Meta): def test_enum_dict_standalone(self): """Test that EnumDict is usable on its own""" - enumdict = EnumDict('test') + enumdict = EnumDict() enumdict['a'] = 1 with self.assertRaises(TypeError):