From 93364f9716614173406a4c83cd624b37d9a02ebf Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 6 May 2022 00:16:22 -0700 Subject: 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 --- Doc/library/enum.rst | 21 +++ Lib/enum.py | 57 ++++++++- Lib/test/test_enum.py | 141 +++++++++++++++++++++ Misc/ACKS | 1 + .../2022-05-05-20-40-45.bpo-78157.IA_9na.rst | 3 + 5 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 52ef009..5db5639 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 85245c9..b9811fe 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -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( diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index b1b8e82..f9e0902 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -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 diff --git a/Misc/ACKS b/Misc/ACKS index 91cd433..a55706d 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 0000000..9e10aca --- /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. -- cgit v0.12