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

View 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(

View 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

View File

@ -1891,6 +1891,7 @@ Jacob Walls
Kevin Walzer
Rodrigo Steinmuller Wanderley
Dingyuan Wang
Edward C Wang
Jiahua Wang
Ke 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.