]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-78157: [Enum] nested classes will not be members in 3.13 (GH-92366)
authorEthan Furman <ethan@stoneleaf.us>
Fri, 6 May 2022 07:16:22 +0000 (00:16 -0700)
committerGitHub <noreply@github.com>
Fri, 6 May 2022 07:16:22 +0000 (00:16 -0700)
- add member() and nonmember() functions
- add deprecation warning for internal classes in enums not
  becoming members in 3.13

Co-authored-by: edwardcwang
Doc/library/enum.rst
Lib/enum.py
Lib/test/test_enum.py
Misc/ACKS
Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst [new file with mode: 0644]

index 52ef0094cb71f276d4a0da977c9676fd86a618a2..5db5639e81a5f0e0c34741c78fbe448221cf3cdd 100644 (file)
@@ -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
index 85245c95f9a9c715abb855303c4f459c8dd75e32..b9811fe9e6787c383c00b991bfc95917fac011e6 100644 (file)
@@ -8,7 +8,7 @@ from functools import reduce
 __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 @@ __all__ = [
 # 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 @@ class _EnumDict(dict):
 
         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 @@ class _EnumDict(dict):
             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(
index b1b8e82b3859f2979a67447584a3b97d7b2907dc..f9e09027228b4e521c84144a2f4de4696a61291b 100644 (file)
@@ -12,6 +12,7 @@ from datetime import date
 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 @@ class TestSpecial(unittest.TestCase):
             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
index 91cd4332d60465f60986785ea03472c48cf93d0c..a55706d508a41d3ef97c99717ddf3cbbb0637853 100644 (file)
--- 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 (file)
index 0000000..9e10aca
--- /dev/null
@@ -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.