]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112328: [Enum] Make some private attributes public. (GH-112514)
authorEthan Furman <ethan@stoneleaf.us>
Tue, 5 Dec 2023 16:27:36 +0000 (08:27 -0800)
committerGitHub <noreply@github.com>
Tue, 5 Dec 2023 16:27:36 +0000 (08:27 -0800)
* [Enum] Make some private attributes public.

- ``_EnumDict`` --> ``EnumDict``
- ``EnumDict._member_names`` --> ``EnumDict.member_names``
- ``Enum._add_alias_``
- ``Enum._add_value_alias_``

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
Doc/howto/enum.rst
Doc/library/enum.rst
Lib/enum.py
Lib/test/test_enum.py
Misc/NEWS.d/next/Library/2023-11-28-20-47-39.gh-issue-112328.Z2AxEY.rst [new file with mode: 0644]

index ffdafb749c73a922ab819ade1b312f5892ecaf46..1e9ac9b6761b64738eb61df91d28a1586d85b7b7 100644 (file)
@@ -868,7 +868,7 @@ Others
 While :class:`IntEnum` is part of the :mod:`enum` module, it would be very
 simple to implement independently::
 
-    class IntEnum(int, Enum):
+    class IntEnum(int, ReprEnum):   # or Enum instead of ReprEnum
         pass
 
 This demonstrates how similar derived enumerations can be defined; for example
@@ -876,8 +876,8 @@ a :class:`FloatEnum` that mixes in :class:`float` instead of :class:`int`.
 
 Some rules:
 
-1. When subclassing :class:`Enum`, mix-in types must appear before
-   :class:`Enum` itself in the sequence of bases, as in the :class:`IntEnum`
+1. When subclassing :class:`Enum`, mix-in types must appear before the
+   :class:`Enum` class itself in the sequence of bases, as in the :class:`IntEnum`
    example above.
 2. Mix-in types must be subclassable. For example, :class:`bool` and
    :class:`range` are not subclassable and will throw an error during Enum
@@ -961,30 +961,34 @@ all the members are created it is no longer used.
 Supported ``_sunder_`` names
 """"""""""""""""""""""""""""
 
-- ``_name_`` -- name of the member
-- ``_value_`` -- value of the member; can be set / modified in ``__new__``
+- :attr:`~Enum._name_` -- name of the member
+- :attr:`~Enum._value_` -- value of the member; can be set in ``__new__``
+- :meth:`~Enum._missing_` -- a lookup function used when a value is not found;
+  may be overridden
+- :attr:`~Enum._ignore_` -- a list of names, either as a :class:`list` or a
+  :class:`str`, that will not be transformed into members, and will be removed
+  from the final class
+- :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for
+  an enum member; may be overridden
+- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing
+  member.
+- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an
+  existing member.  See `MultiValueEnum`_ for an example.
 
-- ``_missing_`` -- a lookup function used when a value is not found; may be
-  overridden
-- ``_ignore_`` -- a list of names, either as a :class:`list` or a :class:`str`,
-  that will not be transformed into members, and will be removed from the final
-  class
-- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
-  (class attribute, removed during class creation)
-- ``_generate_next_value_`` -- used by the `Functional API`_ and by
-  :class:`auto` to get an appropriate value for an enum member; may be
-  overridden
+  .. note::
 
-.. note::
+     For standard :class:`Enum` classes the next value chosen is the highest
+     value seen incremented by one.
 
-    For standard :class:`Enum` classes the next value chosen is the last value seen
-    incremented by one.
+     For :class:`Flag` classes the next value chosen will be the next highest
+     power-of-two.
 
-    For :class:`Flag` classes the next value chosen will be the next highest
-    power-of-two, regardless of the last value seen.
+  .. versionchanged:: 3.13
+     Prior versions would use the last seen value instead of the highest value.
 
 .. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
 .. versionadded:: 3.7 ``_ignore_``
+.. versionadded:: 3.13 ``_add_alias_``, ``_add_value_alias_``
 
 To help keep Python 2 / Python 3 code in sync an :attr:`_order_` attribute can
 be provided.  It will be checked against the actual order of the enumeration
@@ -1447,6 +1451,29 @@ alias::
     disallowing aliases, the :func:`unique` decorator can be used instead.
 
 
+MultiValueEnum
+^^^^^^^^^^^^^^^^^
+
+Supports having more than one value per member::
+
+    >>> class MultiValueEnum(Enum):
+    ...     def __new__(cls, value, *values):
+    ...         self = object.__new__(cls)
+    ...         self._value_ = value
+    ...         for v in values:
+    ...             self._add_value_alias_(v)
+    ...         return self
+    ...
+    >>> class DType(MultiValueEnum):
+    ...     float32 = 'f', 8
+    ...     double64 = 'd', 9
+    ...
+    >>> DType('f')
+    <DType.float32: 'f'>
+    >>> DType(9)
+    <DType.double64: 'd'>
+
+
 Planet
 ^^^^^^
 
index 2d5ae361c3f1e3a0225919a4fcf63159c5e87395..20222bfb3611ab93183bae0acae373fe5b2d5f6f 100644 (file)
@@ -235,6 +235,10 @@ Data Types
         >>> len(Color)
         3
 
+   .. attribute:: EnumType.__members__
+
+      Returns a mapping of every enum name to its member, including aliases
+
    .. method:: EnumType.__reversed__(cls)
 
       Returns each member in *cls* in reverse definition order::
@@ -242,9 +246,19 @@ Data Types
         >>> list(reversed(Color))
         [<Color.BLUE: 3>, <Color.GREEN: 2>, <Color.RED: 1>]
 
+   .. method:: EnumType._add_alias_
+
+      Adds a new name as an alias to an existing member.  Raises a
+      :exc:`NameError` if the name is already assigned to a different member.
+
+   .. method:: EnumType._add_value_alias_
+
+      Adds a new value as an alias to an existing member.  Raises a
+      :exc:`ValueError` if the value is already linked with a different member.
+
    .. versionadded:: 3.11
 
-      Before 3.11 ``enum`` used ``EnumMeta`` type, which is kept as an alias.
+      Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias.
 
 
 .. class:: Enum
@@ -323,7 +337,7 @@ Data Types
          >>> PowersOfThree.SECOND.value
          9
 
-   .. method:: Enum.__init_subclass__(cls, **kwds)
+   .. method:: Enum.__init_subclass__(cls, \**kwds)
 
       A *classmethod* that is used to further configure subsequent subclasses.
       By default, does nothing.
@@ -549,7 +563,7 @@ Data Types
 
    .. method:: __invert__(self):
 
-      Returns all the flags in *type(self)* that are not in self::
+      Returns all the flags in *type(self)* that are not in *self*::
 
          >>> ~white
          <Color: 0>
@@ -769,37 +783,41 @@ Supported ``__dunder__`` names
 :attr:`~EnumType.__members__` is a read-only ordered mapping of ``member_name``:``member``
 items.  It is only available on the class.
 
-:meth:`~object.__new__`, if specified, must create and return the enum members; it is
-also a very good idea to set the member's :attr:`!_value_` appropriately.  Once
-all the members are created it is no longer used.
+:meth:`~object.__new__`, if specified, must create and return the enum members;
+it is also a very good idea to set the member's :attr:`!_value_` appropriately.
+Once all the members are created it is no longer used.
 
 
 Supported ``_sunder_`` names
 """"""""""""""""""""""""""""
 
-- ``_name_`` -- name of the member
-- ``_value_`` -- value of the member; can be set / modified in ``__new__``
-
-- ``_missing_`` -- a lookup function used when a value is not found; may be
-  overridden
-- ``_ignore_`` -- a list of names, either as a :class:`list` or a :class:`str`,
-  that will not be transformed into members, and will be removed from the final
-  class
-- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
-  (class attribute, removed during class creation)
-- ``_generate_next_value_`` -- used to get an appropriate value for an enum
-  member; may be overridden
+- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing
+  member.
+- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an
+  existing member.
+- :attr:`~Enum._name_` -- name of the member
+- :attr:`~Enum._value_` -- value of the member; can be set in ``__new__``
+- :meth:`~Enum._missing_` -- a lookup function used when a value is not found;
+  may be overridden
+- :attr:`~Enum._ignore_` -- a list of names, either as a :class:`list` or a
+  :class:`str`, that will not be transformed into members, and will be removed
+  from the final class
+- :attr:`~Enum._order_` -- used in Python 2/3 code to ensure member order is
+  consistent (class attribute, removed during class creation)
+- :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for
+  an enum member; may be overridden
 
   .. note::
 
-     For standard :class:`Enum` classes the next value chosen is the last value seen
-     incremented by one.
+     For standard :class:`Enum` classes the next value chosen is the highest
+     value seen incremented by one.
 
      For :class:`Flag` classes the next value chosen will be the next highest
-     power-of-two, regardless of the last value seen.
+     power-of-two.
 
 .. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
 .. versionadded:: 3.7 ``_ignore_``
+.. versionadded:: 3.13 ``_add_alias_``, ``_add_value_alias_``
 
 ---------------
 
index 648401e80be685b06aef16c87dd8df3a895b886d..a8a50a5838037585dd62c9d5d97ffc28b09d4801 100644 (file)
@@ -4,7 +4,7 @@ from types import MappingProxyType, DynamicClassAttribute
 
 
 __all__ = [
-        'EnumType', 'EnumMeta',
+        'EnumType', 'EnumMeta', 'EnumDict',
         'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
         'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
         'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
@@ -313,45 +313,8 @@ class _proto_member:
                 ):
                 # no other instances found, record this member in _member_names_
                 enum_class._member_names_.append(member_name)
-        # if necessary, get redirect in place and then add it to _member_map_
-        found_descriptor = None
-        descriptor_type = None
-        class_type = None
-        for base in enum_class.__mro__[1:]:
-            attr = base.__dict__.get(member_name)
-            if attr is not None:
-                if isinstance(attr, (property, DynamicClassAttribute)):
-                    found_descriptor = attr
-                    class_type = base
-                    descriptor_type = 'enum'
-                    break
-                elif _is_descriptor(attr):
-                    found_descriptor = attr
-                    descriptor_type = descriptor_type or 'desc'
-                    class_type = class_type or base
-                    continue
-                else:
-                    descriptor_type = 'attr'
-                    class_type = base
-        if found_descriptor:
-            redirect = property()
-            redirect.member = enum_member
-            redirect.__set_name__(enum_class, member_name)
-            if descriptor_type in ('enum','desc'):
-                # earlier descriptor found; copy fget, fset, fdel to this one.
-                redirect.fget = getattr(found_descriptor, 'fget', None)
-                redirect._get = getattr(found_descriptor, '__get__', None)
-                redirect.fset = getattr(found_descriptor, 'fset', None)
-                redirect._set = getattr(found_descriptor, '__set__', None)
-                redirect.fdel = getattr(found_descriptor, 'fdel', None)
-                redirect._del = getattr(found_descriptor, '__delete__', None)
-            redirect._attr_type = descriptor_type
-            redirect._cls_type = class_type
-            setattr(enum_class, member_name, redirect)
-        else:
-            setattr(enum_class, member_name, enum_member)
-        # now add to _member_map_ (even aliases)
-        enum_class._member_map_[member_name] = enum_member
+
+        enum_class._add_member_(member_name, enum_member)
         try:
             # This may fail if value is not hashable. We can't add the value
             # to the map, and by-value lookups for this value will be
@@ -360,9 +323,10 @@ class _proto_member:
         except TypeError:
             # keep track of the value in a list so containment checks are quick
             enum_class._unhashable_values_.append(value)
+            enum_class._unhashable_values_map_.setdefault(member_name, []).append(value)
 
 
-class _EnumDict(dict):
+class EnumDict(dict):
     """
     Track enum member order and ensure member names are not reused.
 
@@ -371,7 +335,7 @@ class _EnumDict(dict):
     """
     def __init__(self):
         super().__init__()
-        self._member_names = {} # use a dict to keep insertion order
+        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
@@ -393,6 +357,7 @@ class _EnumDict(dict):
                     '_order_',
                     '_generate_next_value_', '_numeric_repr_', '_missing_', '_ignore_',
                     '_iter_member_', '_iter_member_by_value_', '_iter_member_by_def_',
+                    '_add_alias_', '_add_value_alias_',
                     ):
                 raise ValueError(
                         '_sunder_ names, such as %r, are reserved for future Enum use'
@@ -468,6 +433,10 @@ class _EnumDict(dict):
                 self._last_values.append(value)
         super().__setitem__(key, value)
 
+    @property
+    def member_names(self):
+        return list(self._member_names)
+
     def update(self, members, **more_members):
         try:
             for name in members.keys():
@@ -478,6 +447,8 @@ class _EnumDict(dict):
         for name, value in more_members.items():
             self[name] = value
 
+_EnumDict = EnumDict        # keep private name for backwards compatibility
+
 
 class EnumType(type):
     """
@@ -489,7 +460,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 = EnumDict()
         enum_dict._cls_name = cls
         # inherit previous flags and _generate_next_value_ function
         member_type, first_enum = metacls._get_mixins_(cls, bases)
@@ -552,6 +523,7 @@ class EnumType(type):
         classdict['_member_map_'] = {}
         classdict['_value2member_map_'] = {}
         classdict['_unhashable_values_'] = []
+        classdict['_unhashable_values_map_'] = {}
         classdict['_member_type_'] = member_type
         # now set the __repr__ for the value
         classdict['_value_repr_'] = metacls._find_data_repr_(cls, bases)
@@ -754,7 +726,10 @@ class EnumType(type):
         """
         if isinstance(value, cls):
             return True
-        return value in cls._value2member_map_ or value in cls._unhashable_values_
+        try:
+            return value in cls._value2member_map_
+        except TypeError:
+            return value in cls._unhashable_values_
 
     def __delattr__(cls, attr):
         # nicer error message when someone tries to delete an attribute
@@ -1050,7 +1025,57 @@ class EnumType(type):
         else:
             use_args = True
         return __new__, save_new, use_args
-EnumMeta = EnumType
+
+    def _add_member_(cls, name, member):
+        # _value_ structures are not updated
+        if name in cls._member_map_:
+            if cls._member_map_[name] is not member:
+                raise NameError('%r is already bound: %r' % (name, cls._member_map_[name]))
+            return
+        #
+        # if necessary, get redirect in place and then add it to _member_map_
+        found_descriptor = None
+        descriptor_type = None
+        class_type = None
+        for base in cls.__mro__[1:]:
+            attr = base.__dict__.get(name)
+            if attr is not None:
+                if isinstance(attr, (property, DynamicClassAttribute)):
+                    found_descriptor = attr
+                    class_type = base
+                    descriptor_type = 'enum'
+                    break
+                elif _is_descriptor(attr):
+                    found_descriptor = attr
+                    descriptor_type = descriptor_type or 'desc'
+                    class_type = class_type or base
+                    continue
+                else:
+                    descriptor_type = 'attr'
+                    class_type = base
+        if found_descriptor:
+            redirect = property()
+            redirect.member = member
+            redirect.__set_name__(cls, name)
+            if descriptor_type in ('enum', 'desc'):
+                # earlier descriptor found; copy fget, fset, fdel to this one.
+                redirect.fget = getattr(found_descriptor, 'fget', None)
+                redirect._get = getattr(found_descriptor, '__get__', None)
+                redirect.fset = getattr(found_descriptor, 'fset', None)
+                redirect._set = getattr(found_descriptor, '__set__', None)
+                redirect.fdel = getattr(found_descriptor, 'fdel', None)
+                redirect._del = getattr(found_descriptor, '__delete__', None)
+            redirect._attr_type = descriptor_type
+            redirect._cls_type = class_type
+            setattr(cls, name, redirect)
+        else:
+            setattr(cls, name, member)
+        # now add to _member_map_ (even aliases)
+        cls._member_map_[name] = member
+        #
+        cls._member_map_[name] = member
+
+EnumMeta = EnumType         # keep EnumMeta name for backwards compatibility
 
 
 class Enum(metaclass=EnumType):
@@ -1116,9 +1141,9 @@ class Enum(metaclass=EnumType):
             pass
         except TypeError:
             # not there, now do long search -- O(n) behavior
-            for member in cls._member_map_.values():
-                if member._value_ == value:
-                    return member
+            for name, values in cls._unhashable_values_map_.items():
+                if value in values:
+                    return cls[name]
         # still not found -- verify that members exist, in-case somebody got here mistakenly
         # (such as via super when trying to override __new__)
         if not cls._member_map_:
@@ -1159,6 +1184,33 @@ class Enum(metaclass=EnumType):
     def __init__(self, *args, **kwds):
         pass
 
+    def _add_alias_(self, name):
+        self.__class__._add_member_(name, self)
+
+    def _add_value_alias_(self, value):
+        cls = self.__class__
+        try:
+            if value in cls._value2member_map_:
+                if cls._value2member_map_[value] is not self:
+                    raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
+                return
+        except TypeError:
+            # unhashable value, do long search
+            for m in cls._member_map_.values():
+                if m._value_ == value:
+                    if m is not self:
+                        raise ValueError('%r is already bound: %r' % (value, cls._value2member_map_[value]))
+                    return
+        try:
+            # This may fail if value is not hashable. We can't add the value
+            # to the map, and by-value lookups for this value will be
+            # linear.
+            cls._value2member_map_.setdefault(value, self)
+        except TypeError:
+            # keep track of the value in a list so containment checks are quick
+            cls._unhashable_values_.append(value)
+            cls._unhashable_values_map_.setdefault(self.name, []).append(value)
+
     @staticmethod
     def _generate_next_value_(name, start, count, last_values):
         """
@@ -1671,7 +1723,8 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
         body['_member_names_'] = member_names = []
         body['_member_map_'] = member_map = {}
         body['_value2member_map_'] = value2member_map = {}
-        body['_unhashable_values_'] = []
+        body['_unhashable_values_'] = unhashable_values = []
+        body['_unhashable_values_map_'] = {}
         body['_member_type_'] = member_type = etype._member_type_
         body['_value_repr_'] = etype._value_repr_
         if issubclass(etype, Flag):
@@ -1718,14 +1771,9 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
             for name, value in attrs.items():
                 if isinstance(value, auto) and auto.value is _auto_null:
                     value = gnv(name, 1, len(member_names), gnv_last_values)
-                if value in value2member_map:
+                if value in value2member_map or value in unhashable_values:
                     # an alias to an existing member
-                    member = value2member_map[value]
-                    redirect = property()
-                    redirect.member = member
-                    redirect.__set_name__(enum_class, name)
-                    setattr(enum_class, name, redirect)
-                    member_map[name] = member
+                    enum_class(value)._add_alias_(name)
                 else:
                     # create the member
                     if use_args:
@@ -1740,12 +1788,12 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
                     member._name_ = name
                     member.__objclass__ = enum_class
                     member.__init__(value)
-                    redirect = property()
-                    redirect.member = member
-                    redirect.__set_name__(enum_class, name)
-                    setattr(enum_class, name, redirect)
-                    member_map[name] = member
                     member._sort_order_ = len(member_names)
+                    if name not in ('name', 'value'):
+                        setattr(enum_class, name, member)
+                        member_map[name] = member
+                    else:
+                        enum_class._add_member_(name, member)
                     value2member_map[value] = member
                     if _is_single_bit(value):
                         # not a multi-bit alias, record in _member_names_ and _flag_mask_
@@ -1768,14 +1816,13 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
                     if value.value is _auto_null:
                         value.value = gnv(name, 1, len(member_names), gnv_last_values)
                     value = value.value
-                if value in value2member_map:
+                try:
+                    contained = value in value2member_map
+                except TypeError:
+                    contained = value in unhashable_values
+                if contained:
                     # an alias to an existing member
-                    member = value2member_map[value]
-                    redirect = property()
-                    redirect.member = member
-                    redirect.__set_name__(enum_class, name)
-                    setattr(enum_class, name, redirect)
-                    member_map[name] = member
+                    enum_class(value)._add_alias_(name)
                 else:
                     # create the member
                     if use_args:
@@ -1791,14 +1838,22 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
                     member.__objclass__ = enum_class
                     member.__init__(value)
                     member._sort_order_ = len(member_names)
-                    redirect = property()
-                    redirect.member = member
-                    redirect.__set_name__(enum_class, name)
-                    setattr(enum_class, name, redirect)
-                    member_map[name] = member
-                    value2member_map[value] = member
+                    if name not in ('name', 'value'):
+                        setattr(enum_class, name, member)
+                        member_map[name] = member
+                    else:
+                        enum_class._add_member_(name, member)
                     member_names.append(name)
                     gnv_last_values.append(value)
+                    try:
+                        # This may fail if value is not hashable. We can't add the value
+                        # to the map, and by-value lookups for this value will be
+                        # linear.
+                        enum_class._value2member_map_.setdefault(value, member)
+                    except TypeError:
+                        # keep track of the value in a list so containment checks are quick
+                        enum_class._unhashable_values_.append(value)
+                        enum_class._unhashable_values_map_.setdefault(name, []).append(value)
         if '__new__' in body:
             enum_class.__new_member__ = enum_class.__new__
         enum_class.__new__ = Enum.__new__
index c602913ca69277773212bbec4e94260808702084..f99d4ca204b5a7eae0662f9902edc7dfbbfbe3fc 100644 (file)
@@ -514,6 +514,7 @@ class _EnumTests:
             self.assertFalse('first' in MainEnum)
         val = MainEnum.dupe
         self.assertIn(val, MainEnum)
+        self.assertNotIn(float('nan'), MainEnum)
         #
         class OtherEnum(Enum):
             one = auto()
@@ -3268,6 +3269,65 @@ class TestSpecial(unittest.TestCase):
                     member._value_ = Base(value)
                     return member
 
+    def test_extra_member_creation(self):
+        class IDEnumMeta(EnumMeta):
+            def __new__(metacls, cls, bases, classdict, **kwds):
+                # add new entries to classdict
+                for name in classdict.member_names:
+                    classdict[f'{name}_DESC'] = f'-{classdict[name]}'
+                return super().__new__(metacls, cls, bases, classdict, **kwds)
+        class IDEnum(StrEnum, metaclass=IDEnumMeta):
+            pass
+        class MyEnum(IDEnum):
+            ID = 'id'
+            NAME = 'name'
+        self.assertEqual(list(MyEnum), [MyEnum.ID, MyEnum.NAME, MyEnum.ID_DESC, MyEnum.NAME_DESC])
+
+    def test_add_alias(self):
+        class mixin:
+            @property
+            def ORG(self):
+                return 'huh'
+        class Color(mixin, Enum):
+            RED = 1
+            GREEN = 2
+            BLUE = 3
+        Color.RED._add_alias_('ROJO')
+        self.assertIs(Color.RED, Color['ROJO'])
+        self.assertIs(Color.RED, Color.ROJO)
+        Color.BLUE._add_alias_('ORG')
+        self.assertIs(Color.BLUE, Color['ORG'])
+        self.assertIs(Color.BLUE, Color.ORG)
+        self.assertEqual(Color.RED.ORG, 'huh')
+        self.assertEqual(Color.GREEN.ORG, 'huh')
+        self.assertEqual(Color.BLUE.ORG, 'huh')
+        self.assertEqual(Color.ORG.ORG, 'huh')
+
+    def test_add_value_alias_after_creation(self):
+        class Color(Enum):
+            RED = 1
+            GREEN = 2
+            BLUE = 3
+        Color.RED._add_value_alias_(5)
+        self.assertIs(Color.RED, Color(5))
+
+    def test_add_value_alias_during_creation(self):
+        class Types(Enum):
+            Unknown = 0,
+            Source  = 1, 'src'
+            NetList = 2, 'nl'
+            def __new__(cls, int_value, *value_aliases):
+                member = object.__new__(cls)
+                member._value_ = int_value
+                for alias in value_aliases:
+                    member._add_value_alias_(alias)
+                return member
+        self.assertIs(Types(0), Types.Unknown)
+        self.assertIs(Types(1), Types.Source)
+        self.assertIs(Types('src'), Types.Source)
+        self.assertIs(Types(2), Types.NetList)
+        self.assertIs(Types('nl'), Types.NetList)
+
 
 class TestOrder(unittest.TestCase):
     "test usage of the `_order_` attribute"
@@ -4941,12 +5001,14 @@ class TestStdLib(unittest.TestCase):
             @bltns.property
             def zeroth(self):
                 return 'zeroed %s' % self.name
-        self.assertTrue(_test_simple_enum(CheckedColor, SimpleColor) is None)
+        _test_simple_enum(CheckedColor, SimpleColor)
         SimpleColor.MAGENTA._value_ = 9
         self.assertRaisesRegex(
                 TypeError, "enum mismatch",
                 _test_simple_enum, CheckedColor, SimpleColor,
                 )
+        #
+        #
         class CheckedMissing(IntFlag, boundary=KEEP):
             SIXTY_FOUR = 64
             ONE_TWENTY_EIGHT = 128
@@ -4963,8 +5025,28 @@ class TestStdLib(unittest.TestCase):
             ALL = 2048 + 128 + 64 + 12
         M = Missing
         self.assertEqual(list(CheckedMissing), [M.SIXTY_FOUR, M.ONE_TWENTY_EIGHT, M.TWENTY_FORTY_EIGHT])
-        #
         _test_simple_enum(CheckedMissing, Missing)
+        #
+        #
+        class CheckedUnhashable(Enum):
+            ONE = dict()
+            TWO = set()
+            name = 'python'
+        self.assertIn(dict(), CheckedUnhashable)
+        self.assertIn('python', CheckedUnhashable)
+        self.assertEqual(CheckedUnhashable.name.value, 'python')
+        self.assertEqual(CheckedUnhashable.name.name, 'name')
+        #
+        @_simple_enum()
+        class Unhashable:
+            ONE = dict()
+            TWO = set()
+            name = 'python'
+        self.assertIn(dict(), Unhashable)
+        self.assertIn('python', Unhashable)
+        self.assertEqual(Unhashable.name.value, 'python')
+        self.assertEqual(Unhashable.name.name, 'name')
+        _test_simple_enum(Unhashable, Unhashable)
 
 
 class MiscTestCase(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2023-11-28-20-47-39.gh-issue-112328.Z2AxEY.rst b/Misc/NEWS.d/next/Library/2023-11-28-20-47-39.gh-issue-112328.Z2AxEY.rst
new file mode 100644 (file)
index 0000000..6e69024
--- /dev/null
@@ -0,0 +1,2 @@
+[Enum] Make ``EnumDict``, ``EnumDict.member_names``,
+``EnumType._add_alias_`` and ``EnumType._add_value_alias_`` public.