[Python-checkins] gh-78157: [Enum] nested classes will not be members in 3.13 (GH-92366)

ethanfurman webhook-mailer at python.org
Fri May 6 03:16:30 EDT 2022


https://github.com/python/cpython/commit/93364f9716614173406a4c83cd624b37d9a02ebf
commit: 93364f9716614173406a4c83cd624b37d9a02ebf
branch: main
author: Ethan Furman <ethan at stoneleaf.us>
committer: ethanfurman <ethan at stoneleaf.us>
date: 2022-05-06T00:16:22-07:00
summary:

gh-78157: [Enum] nested classes will not be members in 3.13 (GH-92366)

- add member() and nonmember() functions
- add deprecation warning for internal classes in enums not
  becoming members in 3.13

Co-authored-by: edwardcwang

files:
A Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
M Doc/library/enum.rst
M Lib/enum.py
M Lib/test/test_enum.py
M Misc/ACKS

diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index 52ef0094cb71f..5db5639e81a5f 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -124,9 +124,18 @@ Module Contents
       Enum class decorator that checks user-selectable constraints on an
       enumeration.
 
+   :func:`member`
+
+      Make `obj` a member.  Can be used as a decorator.
+
+   :func:`nonmember`
+
+      Do not make `obj` a member.  Can be used as a decorator.
+
 
 .. versionadded:: 3.6  ``Flag``, ``IntFlag``, ``auto``
 .. versionadded:: 3.11  ``StrEnum``, ``EnumCheck``, ``FlagBoundary``, ``property``
+.. versionadded:: 3.11  ``member``, ``nonmember``
 
 ---------------
 
@@ -791,6 +800,18 @@ Utilities and Decorators
 
    .. versionadded:: 3.11
 
+.. decorator:: member
+
+   A decorator for use in enums: it's target will become a member.
+
+   .. versionadded:: 3.11
+
+.. decorator:: nonmember
+
+   A decorator for use in enums: it's target will not become a member.
+
+   .. versionadded:: 3.11
+
 ---------------
 
 Notes
diff --git a/Lib/enum.py b/Lib/enum.py
index 85245c95f9a9c..b9811fe9e6787 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -8,7 +8,7 @@
 __all__ = [
         'EnumType', 'EnumMeta',
         'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
-        'auto', 'unique', 'property', 'verify',
+        'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
         'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
         'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum',
         'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE',
@@ -20,6 +20,20 @@
 # This is also why there are checks in EnumType like `if Enum is not None`
 Enum = Flag = EJECT = _stdlib_enums = ReprEnum = None
 
+class nonmember(object):
+    """
+    Protects item from becaming an Enum member during class creation.
+    """
+    def __init__(self, value):
+        self.value = value
+
+class member(object):
+    """
+    Forces item to became an Enum member during class creation.
+    """
+    def __init__(self, value):
+        self.value = value
+
 def _is_descriptor(obj):
     """
     Returns True if obj is a descriptor, False otherwise.
@@ -52,6 +66,15 @@ def _is_sunder(name):
             name[-2:-1] != '_'
             )
 
+def _is_internal_class(cls_name, obj):
+    # do not use `re` as `re` imports `enum`
+    if not isinstance(obj, type):
+        return False
+    qualname = getattr(obj, '__qualname__', '')
+    s_pattern = cls_name + '.' + getattr(obj, '__name__', '')
+    e_pattern = '.' + s_pattern
+    return qualname == s_pattern or qualname.endswith(e_pattern)
+
 def _is_private(cls_name, name):
     # do not use `re` as `re` imports `enum`
     pattern = '_%s__' % (cls_name, )
@@ -139,14 +162,20 @@ def _dedent(text):
         lines[j] = l[i:]
     return '\n'.join(lines)
 
+class _auto_null:
+    def __repr__(self):
+        return '_auto_null'
+_auto_null = _auto_null()
 
-_auto_null = object()
 class auto:
     """
     Instances are replaced with an appropriate value in Enum class suites.
     """
     value = _auto_null
 
+    def __repr__(self):
+        return "auto(%r)" % self.value
+
 class property(DynamicClassAttribute):
     """
     This is a descriptor, used to define attributes that act differently
@@ -325,8 +354,16 @@ def __setitem__(self, key, value):
 
         Single underscore (sunder) names are reserved.
         """
+        if _is_internal_class(self._cls_name, value):
+            import warnings
+            warnings.warn(
+                    "In 3.13 classes created inside an enum will not become a member.  "
+                    "Use the `member` decorator to keep the current behavior.",
+                    DeprecationWarning,
+                    stacklevel=2,
+                    )
         if _is_private(self._cls_name, key):
-            # do nothing, name will be a normal attribute
+            # also do nothing, name will be a normal attribute
             pass
         elif _is_sunder(key):
             if key not in (
@@ -364,10 +401,22 @@ def __setitem__(self, key, value):
             raise TypeError('%r already defined as %r' % (key, self[key]))
         elif key in self._ignore:
             pass
-        elif not _is_descriptor(value):
+        elif isinstance(value, nonmember):
+            # unwrap value here; it won't be processed by the below `else`
+            value = value.value
+        elif _is_descriptor(value):
+            pass
+        # TODO: uncomment next three lines in 3.12
+        # elif _is_internal_class(self._cls_name, value):
+        #     # do nothing, name will be a normal attribute
+        #     pass
+        else:
             if key in self:
                 # enum overwriting a descriptor?
                 raise TypeError('%r already defined as %r' % (key, self[key]))
+            elif isinstance(value, member):
+                # unwrap value here -- it will become a member
+                value = value.value
             if isinstance(value, auto):
                 if value.value == _auto_null:
                     value.value = self._generate_next_value(
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index b1b8e82b3859f..f9e09027228b4 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -12,6 +12,7 @@
 from enum import Enum, 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
 from io import StringIO
 from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
 from test import support
@@ -938,6 +939,146 @@ def test_enum_function_with_qualname(self):
             raise Theory
         self.assertEqual(Theory.__qualname__, 'spanish_inquisition')
 
+    def test_enum_of_types(self):
+        """Support using Enum to refer to types deliberately."""
+        class MyTypes(Enum):
+            i = int
+            f = float
+            s = str
+        self.assertEqual(MyTypes.i.value, int)
+        self.assertEqual(MyTypes.f.value, float)
+        self.assertEqual(MyTypes.s.value, str)
+        class Foo:
+            pass
+        class Bar:
+            pass
+        class MyTypes2(Enum):
+            a = Foo
+            b = Bar
+        self.assertEqual(MyTypes2.a.value, Foo)
+        self.assertEqual(MyTypes2.b.value, Bar)
+        class SpamEnumNotInner:
+            pass
+        class SpamEnum(Enum):
+            spam = SpamEnumNotInner
+        self.assertEqual(SpamEnum.spam.value, SpamEnumNotInner)
+
+    @unittest.skipIf(
+            python_version >= (3, 13),
+            'inner classes are not members',
+            )
+    def test_nested_classes_in_enum_are_members(self):
+        """
+        Check for warnings pre-3.13
+        """
+        with self.assertWarnsRegex(DeprecationWarning, 'will not become a member'):
+            class Outer(Enum):
+                a = 1
+                b = 2
+                class Inner(Enum):
+                    foo = 10
+                    bar = 11
+        self.assertTrue(isinstance(Outer.Inner, Outer))
+        self.assertEqual(Outer.a.value, 1)
+        self.assertEqual(Outer.Inner.value.foo.value, 10)
+        self.assertEqual(
+            list(Outer.Inner.value),
+            [Outer.Inner.value.foo, Outer.Inner.value.bar],
+            )
+        self.assertEqual(
+            list(Outer),
+            [Outer.a, Outer.b, Outer.Inner],
+            )
+
+    @unittest.skipIf(
+            python_version < (3, 13),
+            'inner classes are still members',
+            )
+    def test_nested_classes_in_enum_are_not_members(self):
+        """Support locally-defined nested classes."""
+        class Outer(Enum):
+            a = 1
+            b = 2
+            class Inner(Enum):
+                foo = 10
+                bar = 11
+        self.assertTrue(isinstance(Outer.Inner, type))
+        self.assertEqual(Outer.a.value, 1)
+        self.assertEqual(Outer.Inner.foo.value, 10)
+        self.assertEqual(
+            list(Outer.Inner),
+            [Outer.Inner.foo, Outer.Inner.bar],
+            )
+        self.assertEqual(
+            list(Outer),
+            [Outer.a, Outer.b],
+            )
+
+    def test_nested_classes_in_enum_with_nonmember(self):
+        class Outer(Enum):
+            a = 1
+            b = 2
+            @nonmember
+            class Inner(Enum):
+                foo = 10
+                bar = 11
+        self.assertTrue(isinstance(Outer.Inner, type))
+        self.assertEqual(Outer.a.value, 1)
+        self.assertEqual(Outer.Inner.foo.value, 10)
+        self.assertEqual(
+            list(Outer.Inner),
+            [Outer.Inner.foo, Outer.Inner.bar],
+            )
+        self.assertEqual(
+            list(Outer),
+            [Outer.a, Outer.b],
+            )
+
+    def test_enum_of_types_with_nonmember(self):
+        """Support using Enum to refer to types deliberately."""
+        class MyTypes(Enum):
+            i = int
+            f = nonmember(float)
+            s = str
+        self.assertEqual(MyTypes.i.value, int)
+        self.assertTrue(MyTypes.f is float)
+        self.assertEqual(MyTypes.s.value, str)
+        class Foo:
+            pass
+        class Bar:
+            pass
+        class MyTypes2(Enum):
+            a = Foo
+            b = nonmember(Bar)
+        self.assertEqual(MyTypes2.a.value, Foo)
+        self.assertTrue(MyTypes2.b is Bar)
+        class SpamEnumIsInner:
+            pass
+        class SpamEnum(Enum):
+            spam = nonmember(SpamEnumIsInner)
+        self.assertTrue(SpamEnum.spam is SpamEnumIsInner)
+
+    def test_nested_classes_in_enum_with_member(self):
+        """Support locally-defined nested classes."""
+        class Outer(Enum):
+            a = 1
+            b = 2
+            @member
+            class Inner(Enum):
+                foo = 10
+                bar = 11
+        self.assertTrue(isinstance(Outer.Inner, Outer))
+        self.assertEqual(Outer.a.value, 1)
+        self.assertEqual(Outer.Inner.value.foo.value, 10)
+        self.assertEqual(
+            list(Outer.Inner.value),
+            [Outer.Inner.value.foo, Outer.Inner.value.bar],
+            )
+        self.assertEqual(
+            list(Outer),
+            [Outer.a, Outer.b, Outer.Inner],
+            )
+
     def test_enum_with_value_name(self):
         class Huh(Enum):
             name = 1
diff --git a/Misc/ACKS b/Misc/ACKS
index 91cd4332d6046..a55706d508a41 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1891,6 +1891,7 @@ Jacob Walls
 Kevin Walzer
 Rodrigo Steinmuller Wanderley
 Dingyuan Wang
+Edward C Wang
 Jiahua Wang
 Ke Wang
 Liang-Bo Wang
diff --git a/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst b/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
new file mode 100644
index 0000000000000..9e10acaf9a1e6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
@@ -0,0 +1,3 @@
+Deprecate nested classes in enum definitions becoming members -- in 3.13
+they will be normal classes; add `member` and `nonmember` functions to allow
+control over results now.



More information about the Python-checkins mailing list