Skip to content

Commit

Permalink
pythongh-112328: Make EnumDict usable on its own and document it (pyt…
Browse files Browse the repository at this point in the history
…honGH-123669)

Co-authored-by: Rafi <[email protected]>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
Co-authored-by: Ethan Furman <[email protected]>
  • Loading branch information
4 people authored Dec 20, 2024
1 parent 3879ca0 commit 2a66dd3
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 16 deletions.
26 changes: 18 additions & 8 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 @@ -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``

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

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

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

Expand Down
6 changes: 4 additions & 2 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------
Expand Down
10 changes: 5 additions & 5 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
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 @@ -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
Expand Down Expand Up @@ -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):
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.

0 comments on commit 2a66dd3

Please sign in to comment.