Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.13] gh-112328: Make EnumDict usable on its own and document it (GH-123669) #128142

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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``

---------------

Expand Down Expand Up @@ -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

---------------

Expand Down Expand Up @@ -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)
Expand All @@ -975,6 +999,7 @@ Utilities and Decorators

.. versionadded:: 3.11


---------------

Notes
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
10 changes: 5 additions & 5 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -359,7 +360,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):
Expand Down Expand Up @@ -413,7 +414,7 @@ def __setitem__(self, key, value):
'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:
Expand Down Expand Up @@ -485,8 +486,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:
Expand Down
33 changes: 32 additions & 1 deletion Lib/test/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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
Expand Down Expand Up @@ -5454,6 +5454,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:class:`enum.EnumDict` can now be used without resorting to private API.
Loading