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
This commit is contained in:
Ethan Furman 2022-05-06 00:16:22 -07:00 committed by GitHub
parent fa4f0a134e
commit 93364f9716
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 219 additions and 4 deletions

View file

@ -124,9 +124,18 @@ Module Contents
Enum class decorator that checks user-selectable constraints on an Enum class decorator that checks user-selectable constraints on an
enumeration. 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.6 ``Flag``, ``IntFlag``, ``auto``
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``FlagBoundary``, ``property`` .. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``FlagBoundary``, ``property``
.. versionadded:: 3.11 ``member``, ``nonmember``
--------------- ---------------
@ -791,6 +800,18 @@ Utilities and Decorators
.. versionadded:: 3.11 .. 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 Notes

View file

@ -8,7 +8,7 @@ from functools import reduce
__all__ = [ __all__ = [
'EnumType', 'EnumMeta', 'EnumType', 'EnumMeta',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum', 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
'auto', 'unique', 'property', 'verify', 'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP', 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum', 'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum',
'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE', '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` # This is also why there are checks in EnumType like `if Enum is not None`
Enum = Flag = EJECT = _stdlib_enums = ReprEnum = 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): def _is_descriptor(obj):
""" """
Returns True if obj is a descriptor, False otherwise. Returns True if obj is a descriptor, False otherwise.
@ -52,6 +66,15 @@ def _is_sunder(name):
name[-2:-1] != '_' 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): def _is_private(cls_name, name):
# do not use `re` as `re` imports `enum` # do not use `re` as `re` imports `enum`
pattern = '_%s__' % (cls_name, ) pattern = '_%s__' % (cls_name, )
@ -139,14 +162,20 @@ def _dedent(text):
lines[j] = l[i:] lines[j] = l[i:]
return '\n'.join(lines) return '\n'.join(lines)
class _auto_null:
def __repr__(self):
return '_auto_null'
_auto_null = _auto_null()
_auto_null = object()
class auto: class auto:
""" """
Instances are replaced with an appropriate value in Enum class suites. Instances are replaced with an appropriate value in Enum class suites.
""" """
value = _auto_null value = _auto_null
def __repr__(self):
return "auto(%r)" % self.value
class property(DynamicClassAttribute): class property(DynamicClassAttribute):
""" """
This is a descriptor, used to define attributes that act differently This is a descriptor, used to define attributes that act differently
@ -325,8 +354,16 @@ class _EnumDict(dict):
Single underscore (sunder) names are reserved. 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): 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 pass
elif _is_sunder(key): elif _is_sunder(key):
if key not in ( if key not in (
@ -364,10 +401,22 @@ class _EnumDict(dict):
raise TypeError('%r already defined as %r' % (key, self[key])) raise TypeError('%r already defined as %r' % (key, self[key]))
elif key in self._ignore: elif key in self._ignore:
pass 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: if key in self:
# enum overwriting a descriptor? # enum overwriting a descriptor?
raise TypeError('%r already defined as %r' % (key, self[key])) 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 isinstance(value, auto):
if value.value == _auto_null: if value.value == _auto_null:
value.value = self._generate_next_value( value.value = self._generate_next_value(

View file

@ -12,6 +12,7 @@ from datetime import date
from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto 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 STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
from enum import member, nonmember
from io import StringIO from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support from test import support
@ -938,6 +939,146 @@ class TestSpecial(unittest.TestCase):
raise Theory raise Theory
self.assertEqual(Theory.__qualname__, 'spanish_inquisition') 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): def test_enum_with_value_name(self):
class Huh(Enum): class Huh(Enum):
name = 1 name = 1

View file

@ -1891,6 +1891,7 @@ Jacob Walls
Kevin Walzer Kevin Walzer
Rodrigo Steinmuller Wanderley Rodrigo Steinmuller Wanderley
Dingyuan Wang Dingyuan Wang
Edward C Wang
Jiahua Wang Jiahua Wang
Ke Wang Ke Wang
Liang-Bo Wang Liang-Bo Wang

View file

@ -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.