Skip to content

Commit 2a9ab75

Browse files
authored
bpo-44559: [Enum] restore fixes lost in 3.9 reversion (GH-29114)
- fix exception leaks - re-add deprecation warnings
1 parent 64e83c7 commit 2a9ab75

File tree

3 files changed

+194
-42
lines changed

3 files changed

+194
-42
lines changed

Doc/library/enum.rst

+12-2
Original file line numberDiff line numberDiff line change
@@ -1125,9 +1125,9 @@ and raise an error if the two do not match::
11251125
_Private__names
11261126
"""""""""""""""
11271127

1128-
Private names will be normal attributes in Python 3.10 instead of either an error
1128+
Private names will be normal attributes in Python 3.11 instead of either an error
11291129
or a member (depending on if the name ends with an underscore). Using these names
1130-
in 3.9 will issue a :exc:`DeprecationWarning`.
1130+
in 3.10 will issue a :exc:`DeprecationWarning`.
11311131

11321132

11331133
``Enum`` member type
@@ -1150,6 +1150,10 @@ all-uppercase names for members)::
11501150
>>> FieldTypes.size.value
11511151
2
11521152

1153+
.. note::
1154+
1155+
This behavior is deprecated and will be removed in 3.12.
1156+
11531157
.. versionchanged:: 3.5
11541158

11551159

@@ -1200,3 +1204,9 @@ all named flags and all named combinations of flags that are in the value::
12001204
>>> Color(7) # not named combination
12011205
<Color.CYAN|MAGENTA|BLUE|YELLOW|GREEN|RED: 7>
12021206

1207+
.. note::
1208+
1209+
In 3.11 unnamed combinations of flags will only produce the canonical flag
1210+
members (aka single-value flags). So ``Color(7)`` will produce something
1211+
like ``<Color.BLUE|GREEN|RED: 7>``.
1212+

Lib/enum.py

+37-23
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ def _is_sunder(name):
4444
def _is_private(cls_name, name):
4545
# do not use `re` as `re` imports `enum`
4646
pattern = '_%s__' % (cls_name, )
47+
pat_len = len(pattern)
4748
if (
48-
len(name) >= 5
49+
len(name) > pat_len
4950
and name.startswith(pattern)
50-
and name[len(pattern)] != '_'
51+
and name[pat_len:pat_len+1] != ['_']
5152
and (name[-1] != '_' or name[-2] != '_')
5253
):
5354
return True
@@ -392,12 +393,19 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s
392393
start=start,
393394
)
394395

395-
def __contains__(cls, member):
396-
if not isinstance(member, Enum):
396+
def __contains__(cls, obj):
397+
if not isinstance(obj, Enum):
398+
import warnings
399+
warnings.warn(
400+
"in 3.12 __contains__ will no longer raise TypeError, but will return True if\n"
401+
"obj is a member or a member's value",
402+
DeprecationWarning,
403+
stacklevel=2,
404+
)
397405
raise TypeError(
398406
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
399-
type(member).__qualname__, cls.__class__.__qualname__))
400-
return isinstance(member, cls) and member._name_ in cls._member_map_
407+
type(obj).__qualname__, cls.__class__.__qualname__))
408+
return isinstance(obj, cls) and obj._name_ in cls._member_map_
401409

402410
def __delattr__(cls, attr):
403411
# nicer error message when someone tries to delete an attribute
@@ -580,27 +588,27 @@ def _get_mixins_(class_name, bases):
580588
return object, Enum
581589

582590
def _find_data_type(bases):
583-
data_types = []
591+
data_types = set()
584592
for chain in bases:
585593
candidate = None
586594
for base in chain.__mro__:
587595
if base is object:
588596
continue
589597
elif issubclass(base, Enum):
590598
if base._member_type_ is not object:
591-
data_types.append(base._member_type_)
599+
data_types.add(base._member_type_)
592600
break
593601
elif '__new__' in base.__dict__:
594602
if issubclass(base, Enum):
595603
continue
596-
data_types.append(candidate or base)
604+
data_types.add(candidate or base)
597605
break
598606
else:
599607
candidate = candidate or base
600608
if len(data_types) > 1:
601609
raise TypeError('%r: too many data types: %r' % (class_name, data_types))
602610
elif data_types:
603-
return data_types[0]
611+
return data_types.pop()
604612
else:
605613
return None
606614

@@ -693,19 +701,25 @@ def __new__(cls, value):
693701
except Exception as e:
694702
exc = e
695703
result = None
696-
if isinstance(result, cls):
697-
return result
698-
else:
699-
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
700-
if result is None and exc is None:
701-
raise ve_exc
702-
elif exc is None:
703-
exc = TypeError(
704-
'error in %s._missing_: returned %r instead of None or a valid member'
705-
% (cls.__name__, result)
706-
)
707-
exc.__context__ = ve_exc
708-
raise exc
704+
try:
705+
if isinstance(result, cls):
706+
return result
707+
else:
708+
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
709+
if result is None and exc is None:
710+
raise ve_exc
711+
elif exc is None:
712+
exc = TypeError(
713+
'error in %s._missing_: returned %r instead of None or a valid member'
714+
% (cls.__name__, result)
715+
)
716+
if not isinstance(exc, ValueError):
717+
exc.__context__ = ve_exc
718+
raise exc
719+
finally:
720+
# ensure all variables that could hold an exception are destroyed
721+
exc = None
722+
ve_exc = None
709723

710724
def _generate_next_value_(name, start, count, last_values):
711725
"""

Lib/test/test_enum.py

+145-17
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from test.support import ALWAYS_EQ, check__all__, threading_helper
1212
from datetime import timedelta
1313

14+
python_version = sys.version_info[:2]
1415

1516
# for pickle tests
1617
try:
@@ -347,17 +348,38 @@ class IntLogic(int, Enum):
347348
self.assertTrue(IntLogic.true)
348349
self.assertFalse(IntLogic.false)
349350

350-
def test_contains(self):
351+
@unittest.skipIf(
352+
python_version >= (3, 12),
353+
'__contains__ now returns True/False for all inputs',
354+
)
355+
def test_contains_er(self):
351356
Season = self.Season
352357
self.assertIn(Season.AUTUMN, Season)
353358
with self.assertRaises(TypeError):
354-
3 in Season
359+
with self.assertWarns(DeprecationWarning):
360+
3 in Season
355361
with self.assertRaises(TypeError):
356-
'AUTUMN' in Season
357-
362+
with self.assertWarns(DeprecationWarning):
363+
'AUTUMN' in Season
358364
val = Season(3)
359365
self.assertIn(val, Season)
366+
#
367+
class OtherEnum(Enum):
368+
one = 1; two = 2
369+
self.assertNotIn(OtherEnum.two, Season)
360370

371+
@unittest.skipIf(
372+
python_version < (3, 12),
373+
'__contains__ only works with enum memmbers before 3.12',
374+
)
375+
def test_contains_tf(self):
376+
Season = self.Season
377+
self.assertIn(Season.AUTUMN, Season)
378+
self.assertTrue(3 in Season)
379+
self.assertFalse('AUTUMN' in Season)
380+
val = Season(3)
381+
self.assertIn(val, Season)
382+
#
361383
class OtherEnum(Enum):
362384
one = 1; two = 2
363385
self.assertNotIn(OtherEnum.two, Season)
@@ -1932,6 +1954,38 @@ def _missing_(cls, item):
19321954
else:
19331955
raise Exception('Exception not raised.')
19341956

1957+
def test_missing_exceptions_reset(self):
1958+
import weakref
1959+
#
1960+
class TestEnum(enum.Enum):
1961+
VAL1 = 'val1'
1962+
VAL2 = 'val2'
1963+
#
1964+
class Class1:
1965+
def __init__(self):
1966+
# Gracefully handle an exception of our own making
1967+
try:
1968+
raise ValueError()
1969+
except ValueError:
1970+
pass
1971+
#
1972+
class Class2:
1973+
def __init__(self):
1974+
# Gracefully handle an exception of Enum's making
1975+
try:
1976+
TestEnum('invalid_value')
1977+
except ValueError:
1978+
pass
1979+
# No strong refs here so these are free to die.
1980+
class_1_ref = weakref.ref(Class1())
1981+
class_2_ref = weakref.ref(Class2())
1982+
#
1983+
# The exception raised by Enum creates a reference loop and thus
1984+
# Class2 instances will stick around until the next gargage collection
1985+
# cycle, unlike Class1.
1986+
self.assertIs(class_1_ref(), None)
1987+
self.assertIs(class_2_ref(), None)
1988+
19351989
def test_multiple_mixin(self):
19361990
class MaxMixin:
19371991
@classproperty
@@ -2085,7 +2139,7 @@ def test_empty_globals(self):
20852139
exec(code, global_ns, local_ls)
20862140

20872141
@unittest.skipUnless(
2088-
sys.version_info[:2] == (3, 9),
2142+
python_version == (3, 9),
20892143
'private variables are now normal attributes',
20902144
)
20912145
def test_warning_for_private_variables(self):
@@ -2390,19 +2444,42 @@ def test_pickle(self):
23902444
test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE)
23912445
test_pickle_dump_load(self.assertIs, FlagStooges)
23922446

2393-
def test_contains(self):
2447+
@unittest.skipIf(
2448+
python_version >= (3, 12),
2449+
'__contains__ now returns True/False for all inputs',
2450+
)
2451+
def test_contains_er(self):
23942452
Open = self.Open
23952453
Color = self.Color
23962454
self.assertFalse(Color.BLACK in Open)
23972455
self.assertFalse(Open.RO in Color)
23982456
with self.assertRaises(TypeError):
2399-
'BLACK' in Color
2457+
with self.assertWarns(DeprecationWarning):
2458+
'BLACK' in Color
24002459
with self.assertRaises(TypeError):
2401-
'RO' in Open
2460+
with self.assertWarns(DeprecationWarning):
2461+
'RO' in Open
24022462
with self.assertRaises(TypeError):
2403-
1 in Color
2463+
with self.assertWarns(DeprecationWarning):
2464+
1 in Color
24042465
with self.assertRaises(TypeError):
2405-
1 in Open
2466+
with self.assertWarns(DeprecationWarning):
2467+
1 in Open
2468+
2469+
@unittest.skipIf(
2470+
python_version < (3, 12),
2471+
'__contains__ only works with enum memmbers before 3.12',
2472+
)
2473+
def test_contains_tf(self):
2474+
Open = self.Open
2475+
Color = self.Color
2476+
self.assertFalse(Color.BLACK in Open)
2477+
self.assertFalse(Open.RO in Color)
2478+
self.assertFalse('BLACK' in Color)
2479+
self.assertFalse('RO' in Open)
2480+
self.assertTrue(1 in Color)
2481+
self.assertTrue(1 in Open)
2482+
24062483

24072484
def test_member_contains(self):
24082485
Perm = self.Perm
@@ -2883,21 +2960,45 @@ def test_programatic_function_from_empty_tuple(self):
28832960
self.assertEqual(len(lst), len(Thing))
28842961
self.assertEqual(len(Thing), 0, Thing)
28852962

2886-
def test_contains(self):
2963+
@unittest.skipIf(
2964+
python_version >= (3, 12),
2965+
'__contains__ now returns True/False for all inputs',
2966+
)
2967+
def test_contains_er(self):
28872968
Open = self.Open
28882969
Color = self.Color
28892970
self.assertTrue(Color.GREEN in Color)
28902971
self.assertTrue(Open.RW in Open)
28912972
self.assertFalse(Color.GREEN in Open)
28922973
self.assertFalse(Open.RW in Color)
28932974
with self.assertRaises(TypeError):
2894-
'GREEN' in Color
2975+
with self.assertWarns(DeprecationWarning):
2976+
'GREEN' in Color
28952977
with self.assertRaises(TypeError):
2896-
'RW' in Open
2978+
with self.assertWarns(DeprecationWarning):
2979+
'RW' in Open
28972980
with self.assertRaises(TypeError):
2898-
2 in Color
2981+
with self.assertWarns(DeprecationWarning):
2982+
2 in Color
28992983
with self.assertRaises(TypeError):
2900-
2 in Open
2984+
with self.assertWarns(DeprecationWarning):
2985+
2 in Open
2986+
2987+
@unittest.skipIf(
2988+
python_version < (3, 12),
2989+
'__contains__ only works with enum memmbers before 3.12',
2990+
)
2991+
def test_contains_tf(self):
2992+
Open = self.Open
2993+
Color = self.Color
2994+
self.assertTrue(Color.GREEN in Color)
2995+
self.assertTrue(Open.RW in Open)
2996+
self.assertTrue(Color.GREEN in Open)
2997+
self.assertTrue(Open.RW in Color)
2998+
self.assertFalse('GREEN' in Color)
2999+
self.assertFalse('RW' in Open)
3000+
self.assertTrue(2 in Color)
3001+
self.assertTrue(2 in Open)
29013002

29023003
def test_member_contains(self):
29033004
Perm = self.Perm
@@ -3267,7 +3368,7 @@ def test_convert(self):
32673368
if name[0:2] not in ('CO', '__')],
32683369
[], msg='Names other than CONVERT_TEST_* found.')
32693370

3270-
@unittest.skipUnless(sys.version_info[:2] == (3, 8),
3371+
@unittest.skipUnless(python_version == (3, 8),
32713372
'_convert was deprecated in 3.8')
32723373
def test_convert_warn(self):
32733374
with self.assertWarns(DeprecationWarning):
@@ -3276,7 +3377,7 @@ def test_convert_warn(self):
32763377
('test.test_enum', '__main__')[__name__=='__main__'],
32773378
filter=lambda x: x.startswith('CONVERT_TEST_'))
32783379

3279-
@unittest.skipUnless(sys.version_info >= (3, 9),
3380+
@unittest.skipUnless(python_version >= (3, 9),
32803381
'_convert was removed in 3.9')
32813382
def test_convert_raise(self):
32823383
with self.assertRaises(AttributeError):
@@ -3285,6 +3386,33 @@ def test_convert_raise(self):
32853386
('test.test_enum', '__main__')[__name__=='__main__'],
32863387
filter=lambda x: x.startswith('CONVERT_TEST_'))
32873388

3389+
class TestHelpers(unittest.TestCase):
3390+
3391+
sunder_names = '_bad_', '_good_', '_what_ho_'
3392+
dunder_names = '__mal__', '__bien__', '__que_que__'
3393+
private_names = '_MyEnum__private', '_MyEnum__still_private'
3394+
private_and_sunder_names = '_MyEnum__private_', '_MyEnum__also_private_'
3395+
random_names = 'okay', '_semi_private', '_weird__', '_MyEnum__'
3396+
3397+
def test_sunder(self):
3398+
for name in self.sunder_names + self.private_and_sunder_names:
3399+
self.assertTrue(enum._is_sunder(name), '%r is a not sunder name?' % name)
3400+
for name in self.dunder_names + self.private_names + self.random_names:
3401+
self.assertFalse(enum._is_sunder(name), '%r is a sunder name?' % name)
3402+
3403+
def test_dunder(self):
3404+
for name in self.dunder_names:
3405+
self.assertTrue(enum._is_dunder(name), '%r is a not dunder name?' % name)
3406+
for name in self.sunder_names + self.private_names + self.private_and_sunder_names + self.random_names:
3407+
self.assertFalse(enum._is_dunder(name), '%r is a dunder name?' % name)
3408+
3409+
def test_is_private(self):
3410+
for name in self.private_names + self.private_and_sunder_names:
3411+
self.assertTrue(enum._is_private('MyEnum', name), '%r is a not private name?')
3412+
for name in self.sunder_names + self.dunder_names + self.random_names:
3413+
self.assertFalse(enum._is_private('MyEnum', name), '%r is a private name?')
3414+
32883415

32893416
if __name__ == '__main__':
32903417
unittest.main()
3418+

0 commit comments

Comments
 (0)