Skip to content

Commit 93364f9

Browse files
authored
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
1 parent fa4f0a1 commit 93364f9

File tree

5 files changed

+219
-4
lines changed

5 files changed

+219
-4
lines changed

Doc/library/enum.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,18 @@ Module Contents
124124
Enum class decorator that checks user-selectable constraints on an
125125
enumeration.
126126

127+
:func:`member`
128+
129+
Make `obj` a member. Can be used as a decorator.
130+
131+
:func:`nonmember`
132+
133+
Do not make `obj` a member. Can be used as a decorator.
134+
127135

128136
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
129137
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``FlagBoundary``, ``property``
138+
.. versionadded:: 3.11 ``member``, ``nonmember``
130139

131140
---------------
132141

@@ -791,6 +800,18 @@ Utilities and Decorators
791800

792801
.. versionadded:: 3.11
793802

803+
.. decorator:: member
804+
805+
A decorator for use in enums: it's target will become a member.
806+
807+
.. versionadded:: 3.11
808+
809+
.. decorator:: nonmember
810+
811+
A decorator for use in enums: it's target will not become a member.
812+
813+
.. versionadded:: 3.11
814+
794815
---------------
795816

796817
Notes

Lib/enum.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
__all__ = [
99
'EnumType', 'EnumMeta',
1010
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
11-
'auto', 'unique', 'property', 'verify',
11+
'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
1212
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
1313
'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum',
1414
'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE',
@@ -20,6 +20,20 @@
2020
# This is also why there are checks in EnumType like `if Enum is not None`
2121
Enum = Flag = EJECT = _stdlib_enums = ReprEnum = None
2222

23+
class nonmember(object):
24+
"""
25+
Protects item from becaming an Enum member during class creation.
26+
"""
27+
def __init__(self, value):
28+
self.value = value
29+
30+
class member(object):
31+
"""
32+
Forces item to became an Enum member during class creation.
33+
"""
34+
def __init__(self, value):
35+
self.value = value
36+
2337
def _is_descriptor(obj):
2438
"""
2539
Returns True if obj is a descriptor, False otherwise.
@@ -52,6 +66,15 @@ def _is_sunder(name):
5266
name[-2:-1] != '_'
5367
)
5468

69+
def _is_internal_class(cls_name, obj):
70+
# do not use `re` as `re` imports `enum`
71+
if not isinstance(obj, type):
72+
return False
73+
qualname = getattr(obj, '__qualname__', '')
74+
s_pattern = cls_name + '.' + getattr(obj, '__name__', '')
75+
e_pattern = '.' + s_pattern
76+
return qualname == s_pattern or qualname.endswith(e_pattern)
77+
5578
def _is_private(cls_name, name):
5679
# do not use `re` as `re` imports `enum`
5780
pattern = '_%s__' % (cls_name, )
@@ -139,14 +162,20 @@ def _dedent(text):
139162
lines[j] = l[i:]
140163
return '\n'.join(lines)
141164

165+
class _auto_null:
166+
def __repr__(self):
167+
return '_auto_null'
168+
_auto_null = _auto_null()
142169

143-
_auto_null = object()
144170
class auto:
145171
"""
146172
Instances are replaced with an appropriate value in Enum class suites.
147173
"""
148174
value = _auto_null
149175

176+
def __repr__(self):
177+
return "auto(%r)" % self.value
178+
150179
class property(DynamicClassAttribute):
151180
"""
152181
This is a descriptor, used to define attributes that act differently
@@ -325,8 +354,16 @@ def __setitem__(self, key, value):
325354
326355
Single underscore (sunder) names are reserved.
327356
"""
357+
if _is_internal_class(self._cls_name, value):
358+
import warnings
359+
warnings.warn(
360+
"In 3.13 classes created inside an enum will not become a member. "
361+
"Use the `member` decorator to keep the current behavior.",
362+
DeprecationWarning,
363+
stacklevel=2,
364+
)
328365
if _is_private(self._cls_name, key):
329-
# do nothing, name will be a normal attribute
366+
# also do nothing, name will be a normal attribute
330367
pass
331368
elif _is_sunder(key):
332369
if key not in (
@@ -364,10 +401,22 @@ def __setitem__(self, key, value):
364401
raise TypeError('%r already defined as %r' % (key, self[key]))
365402
elif key in self._ignore:
366403
pass
367-
elif not _is_descriptor(value):
404+
elif isinstance(value, nonmember):
405+
# unwrap value here; it won't be processed by the below `else`
406+
value = value.value
407+
elif _is_descriptor(value):
408+
pass
409+
# TODO: uncomment next three lines in 3.12
410+
# elif _is_internal_class(self._cls_name, value):
411+
# # do nothing, name will be a normal attribute
412+
# pass
413+
else:
368414
if key in self:
369415
# enum overwriting a descriptor?
370416
raise TypeError('%r already defined as %r' % (key, self[key]))
417+
elif isinstance(value, member):
418+
# unwrap value here -- it will become a member
419+
value = value.value
371420
if isinstance(value, auto):
372421
if value.value == _auto_null:
373422
value.value = self._generate_next_value(

Lib/test/test_enum.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
1313
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
1414
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
15+
from enum import member, nonmember
1516
from io import StringIO
1617
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
1718
from test import support
@@ -938,6 +939,146 @@ def test_enum_function_with_qualname(self):
938939
raise Theory
939940
self.assertEqual(Theory.__qualname__, 'spanish_inquisition')
940941

942+
def test_enum_of_types(self):
943+
"""Support using Enum to refer to types deliberately."""
944+
class MyTypes(Enum):
945+
i = int
946+
f = float
947+
s = str
948+
self.assertEqual(MyTypes.i.value, int)
949+
self.assertEqual(MyTypes.f.value, float)
950+
self.assertEqual(MyTypes.s.value, str)
951+
class Foo:
952+
pass
953+
class Bar:
954+
pass
955+
class MyTypes2(Enum):
956+
a = Foo
957+
b = Bar
958+
self.assertEqual(MyTypes2.a.value, Foo)
959+
self.assertEqual(MyTypes2.b.value, Bar)
960+
class SpamEnumNotInner:
961+
pass
962+
class SpamEnum(Enum):
963+
spam = SpamEnumNotInner
964+
self.assertEqual(SpamEnum.spam.value, SpamEnumNotInner)
965+
966+
@unittest.skipIf(
967+
python_version >= (3, 13),
968+
'inner classes are not members',
969+
)
970+
def test_nested_classes_in_enum_are_members(self):
971+
"""
972+
Check for warnings pre-3.13
973+
"""
974+
with self.assertWarnsRegex(DeprecationWarning, 'will not become a member'):
975+
class Outer(Enum):
976+
a = 1
977+
b = 2
978+
class Inner(Enum):
979+
foo = 10
980+
bar = 11
981+
self.assertTrue(isinstance(Outer.Inner, Outer))
982+
self.assertEqual(Outer.a.value, 1)
983+
self.assertEqual(Outer.Inner.value.foo.value, 10)
984+
self.assertEqual(
985+
list(Outer.Inner.value),
986+
[Outer.Inner.value.foo, Outer.Inner.value.bar],
987+
)
988+
self.assertEqual(
989+
list(Outer),
990+
[Outer.a, Outer.b, Outer.Inner],
991+
)
992+
993+
@unittest.skipIf(
994+
python_version < (3, 13),
995+
'inner classes are still members',
996+
)
997+
def test_nested_classes_in_enum_are_not_members(self):
998+
"""Support locally-defined nested classes."""
999+
class Outer(Enum):
1000+
a = 1
1001+
b = 2
1002+
class Inner(Enum):
1003+
foo = 10
1004+
bar = 11
1005+
self.assertTrue(isinstance(Outer.Inner, type))
1006+
self.assertEqual(Outer.a.value, 1)
1007+
self.assertEqual(Outer.Inner.foo.value, 10)
1008+
self.assertEqual(
1009+
list(Outer.Inner),
1010+
[Outer.Inner.foo, Outer.Inner.bar],
1011+
)
1012+
self.assertEqual(
1013+
list(Outer),
1014+
[Outer.a, Outer.b],
1015+
)
1016+
1017+
def test_nested_classes_in_enum_with_nonmember(self):
1018+
class Outer(Enum):
1019+
a = 1
1020+
b = 2
1021+
@nonmember
1022+
class Inner(Enum):
1023+
foo = 10
1024+
bar = 11
1025+
self.assertTrue(isinstance(Outer.Inner, type))
1026+
self.assertEqual(Outer.a.value, 1)
1027+
self.assertEqual(Outer.Inner.foo.value, 10)
1028+
self.assertEqual(
1029+
list(Outer.Inner),
1030+
[Outer.Inner.foo, Outer.Inner.bar],
1031+
)
1032+
self.assertEqual(
1033+
list(Outer),
1034+
[Outer.a, Outer.b],
1035+
)
1036+
1037+
def test_enum_of_types_with_nonmember(self):
1038+
"""Support using Enum to refer to types deliberately."""
1039+
class MyTypes(Enum):
1040+
i = int
1041+
f = nonmember(float)
1042+
s = str
1043+
self.assertEqual(MyTypes.i.value, int)
1044+
self.assertTrue(MyTypes.f is float)
1045+
self.assertEqual(MyTypes.s.value, str)
1046+
class Foo:
1047+
pass
1048+
class Bar:
1049+
pass
1050+
class MyTypes2(Enum):
1051+
a = Foo
1052+
b = nonmember(Bar)
1053+
self.assertEqual(MyTypes2.a.value, Foo)
1054+
self.assertTrue(MyTypes2.b is Bar)
1055+
class SpamEnumIsInner:
1056+
pass
1057+
class SpamEnum(Enum):
1058+
spam = nonmember(SpamEnumIsInner)
1059+
self.assertTrue(SpamEnum.spam is SpamEnumIsInner)
1060+
1061+
def test_nested_classes_in_enum_with_member(self):
1062+
"""Support locally-defined nested classes."""
1063+
class Outer(Enum):
1064+
a = 1
1065+
b = 2
1066+
@member
1067+
class Inner(Enum):
1068+
foo = 10
1069+
bar = 11
1070+
self.assertTrue(isinstance(Outer.Inner, Outer))
1071+
self.assertEqual(Outer.a.value, 1)
1072+
self.assertEqual(Outer.Inner.value.foo.value, 10)
1073+
self.assertEqual(
1074+
list(Outer.Inner.value),
1075+
[Outer.Inner.value.foo, Outer.Inner.value.bar],
1076+
)
1077+
self.assertEqual(
1078+
list(Outer),
1079+
[Outer.a, Outer.b, Outer.Inner],
1080+
)
1081+
9411082
def test_enum_with_value_name(self):
9421083
class Huh(Enum):
9431084
name = 1

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,7 @@ Jacob Walls
18911891
Kevin Walzer
18921892
Rodrigo Steinmuller Wanderley
18931893
Dingyuan Wang
1894+
Edward C Wang
18941895
Jiahua Wang
18951896
Ke Wang
18961897
Liang-Bo Wang
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deprecate nested classes in enum definitions becoming members -- in 3.13
2+
they will be normal classes; add `member` and `nonmember` functions to allow
3+
control over results now.

0 commit comments

Comments
 (0)