mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
bpo-44559: [Enum] restore fixes lost in 3.9 reversion (GH-29114)
- fix exception leaks - re-add deprecation warnings
This commit is contained in:
parent
64e83c711e
commit
2a9ab75af3
3 changed files with 194 additions and 42 deletions
|
|
@ -1125,9 +1125,9 @@ and raise an error if the two do not match::
|
||||||
_Private__names
|
_Private__names
|
||||||
"""""""""""""""
|
"""""""""""""""
|
||||||
|
|
||||||
Private names will be normal attributes in Python 3.10 instead of either an error
|
Private names will be normal attributes in Python 3.11 instead of either an error
|
||||||
or a member (depending on if the name ends with an underscore). Using these names
|
or a member (depending on if the name ends with an underscore). Using these names
|
||||||
in 3.9 will issue a :exc:`DeprecationWarning`.
|
in 3.10 will issue a :exc:`DeprecationWarning`.
|
||||||
|
|
||||||
|
|
||||||
``Enum`` member type
|
``Enum`` member type
|
||||||
|
|
@ -1150,6 +1150,10 @@ all-uppercase names for members)::
|
||||||
>>> FieldTypes.size.value
|
>>> FieldTypes.size.value
|
||||||
2
|
2
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This behavior is deprecated and will be removed in 3.12.
|
||||||
|
|
||||||
.. versionchanged:: 3.5
|
.. versionchanged:: 3.5
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1200,3 +1204,9 @@ all named flags and all named combinations of flags that are in the value::
|
||||||
>>> Color(7) # not named combination
|
>>> Color(7) # not named combination
|
||||||
<Color.CYAN|MAGENTA|BLUE|YELLOW|GREEN|RED: 7>
|
<Color.CYAN|MAGENTA|BLUE|YELLOW|GREEN|RED: 7>
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In 3.11 unnamed combinations of flags will only produce the canonical flag
|
||||||
|
members (aka single-value flags). So ``Color(7)`` will produce something
|
||||||
|
like ``<Color.BLUE|GREEN|RED: 7>``.
|
||||||
|
|
||||||
|
|
|
||||||
60
Lib/enum.py
60
Lib/enum.py
|
|
@ -44,10 +44,11 @@ def _is_sunder(name):
|
||||||
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, )
|
||||||
|
pat_len = len(pattern)
|
||||||
if (
|
if (
|
||||||
len(name) >= 5
|
len(name) > pat_len
|
||||||
and name.startswith(pattern)
|
and name.startswith(pattern)
|
||||||
and name[len(pattern)] != '_'
|
and name[pat_len:pat_len+1] != ['_']
|
||||||
and (name[-1] != '_' or name[-2] != '_')
|
and (name[-1] != '_' or name[-2] != '_')
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
@ -392,12 +393,19 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s
|
||||||
start=start,
|
start=start,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __contains__(cls, member):
|
def __contains__(cls, obj):
|
||||||
if not isinstance(member, Enum):
|
if not isinstance(obj, Enum):
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"in 3.12 __contains__ will no longer raise TypeError, but will return True if\n"
|
||||||
|
"obj is a member or a member's value",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
|
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
|
||||||
type(member).__qualname__, cls.__class__.__qualname__))
|
type(obj).__qualname__, cls.__class__.__qualname__))
|
||||||
return isinstance(member, cls) and member._name_ in cls._member_map_
|
return isinstance(obj, cls) and obj._name_ in cls._member_map_
|
||||||
|
|
||||||
def __delattr__(cls, attr):
|
def __delattr__(cls, attr):
|
||||||
# nicer error message when someone tries to delete an attribute
|
# nicer error message when someone tries to delete an attribute
|
||||||
|
|
@ -580,7 +588,7 @@ def _get_mixins_(class_name, bases):
|
||||||
return object, Enum
|
return object, Enum
|
||||||
|
|
||||||
def _find_data_type(bases):
|
def _find_data_type(bases):
|
||||||
data_types = []
|
data_types = set()
|
||||||
for chain in bases:
|
for chain in bases:
|
||||||
candidate = None
|
candidate = None
|
||||||
for base in chain.__mro__:
|
for base in chain.__mro__:
|
||||||
|
|
@ -588,19 +596,19 @@ def _find_data_type(bases):
|
||||||
continue
|
continue
|
||||||
elif issubclass(base, Enum):
|
elif issubclass(base, Enum):
|
||||||
if base._member_type_ is not object:
|
if base._member_type_ is not object:
|
||||||
data_types.append(base._member_type_)
|
data_types.add(base._member_type_)
|
||||||
break
|
break
|
||||||
elif '__new__' in base.__dict__:
|
elif '__new__' in base.__dict__:
|
||||||
if issubclass(base, Enum):
|
if issubclass(base, Enum):
|
||||||
continue
|
continue
|
||||||
data_types.append(candidate or base)
|
data_types.add(candidate or base)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
candidate = candidate or base
|
candidate = candidate or base
|
||||||
if len(data_types) > 1:
|
if len(data_types) > 1:
|
||||||
raise TypeError('%r: too many data types: %r' % (class_name, data_types))
|
raise TypeError('%r: too many data types: %r' % (class_name, data_types))
|
||||||
elif data_types:
|
elif data_types:
|
||||||
return data_types[0]
|
return data_types.pop()
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -693,19 +701,25 @@ def __new__(cls, value):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exc = e
|
exc = e
|
||||||
result = None
|
result = None
|
||||||
if isinstance(result, cls):
|
try:
|
||||||
return result
|
if isinstance(result, cls):
|
||||||
else:
|
return result
|
||||||
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
|
else:
|
||||||
if result is None and exc is None:
|
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
|
||||||
raise ve_exc
|
if result is None and exc is None:
|
||||||
elif exc is None:
|
raise ve_exc
|
||||||
exc = TypeError(
|
elif exc is None:
|
||||||
'error in %s._missing_: returned %r instead of None or a valid member'
|
exc = TypeError(
|
||||||
% (cls.__name__, result)
|
'error in %s._missing_: returned %r instead of None or a valid member'
|
||||||
)
|
% (cls.__name__, result)
|
||||||
exc.__context__ = ve_exc
|
)
|
||||||
raise exc
|
if not isinstance(exc, ValueError):
|
||||||
|
exc.__context__ = ve_exc
|
||||||
|
raise exc
|
||||||
|
finally:
|
||||||
|
# ensure all variables that could hold an exception are destroyed
|
||||||
|
exc = None
|
||||||
|
ve_exc = None
|
||||||
|
|
||||||
def _generate_next_value_(name, start, count, last_values):
|
def _generate_next_value_(name, start, count, last_values):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
from test.support import ALWAYS_EQ, check__all__, threading_helper
|
from test.support import ALWAYS_EQ, check__all__, threading_helper
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
python_version = sys.version_info[:2]
|
||||||
|
|
||||||
# for pickle tests
|
# for pickle tests
|
||||||
try:
|
try:
|
||||||
|
|
@ -347,17 +348,38 @@ class IntLogic(int, Enum):
|
||||||
self.assertTrue(IntLogic.true)
|
self.assertTrue(IntLogic.true)
|
||||||
self.assertFalse(IntLogic.false)
|
self.assertFalse(IntLogic.false)
|
||||||
|
|
||||||
def test_contains(self):
|
@unittest.skipIf(
|
||||||
|
python_version >= (3, 12),
|
||||||
|
'__contains__ now returns True/False for all inputs',
|
||||||
|
)
|
||||||
|
def test_contains_er(self):
|
||||||
Season = self.Season
|
Season = self.Season
|
||||||
self.assertIn(Season.AUTUMN, Season)
|
self.assertIn(Season.AUTUMN, Season)
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
3 in Season
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
3 in Season
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
'AUTUMN' in Season
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
'AUTUMN' in Season
|
||||||
val = Season(3)
|
val = Season(3)
|
||||||
self.assertIn(val, Season)
|
self.assertIn(val, Season)
|
||||||
|
#
|
||||||
|
class OtherEnum(Enum):
|
||||||
|
one = 1; two = 2
|
||||||
|
self.assertNotIn(OtherEnum.two, Season)
|
||||||
|
|
||||||
|
@unittest.skipIf(
|
||||||
|
python_version < (3, 12),
|
||||||
|
'__contains__ only works with enum memmbers before 3.12',
|
||||||
|
)
|
||||||
|
def test_contains_tf(self):
|
||||||
|
Season = self.Season
|
||||||
|
self.assertIn(Season.AUTUMN, Season)
|
||||||
|
self.assertTrue(3 in Season)
|
||||||
|
self.assertFalse('AUTUMN' in Season)
|
||||||
|
val = Season(3)
|
||||||
|
self.assertIn(val, Season)
|
||||||
|
#
|
||||||
class OtherEnum(Enum):
|
class OtherEnum(Enum):
|
||||||
one = 1; two = 2
|
one = 1; two = 2
|
||||||
self.assertNotIn(OtherEnum.two, Season)
|
self.assertNotIn(OtherEnum.two, Season)
|
||||||
|
|
@ -1932,6 +1954,38 @@ def _missing_(cls, item):
|
||||||
else:
|
else:
|
||||||
raise Exception('Exception not raised.')
|
raise Exception('Exception not raised.')
|
||||||
|
|
||||||
|
def test_missing_exceptions_reset(self):
|
||||||
|
import weakref
|
||||||
|
#
|
||||||
|
class TestEnum(enum.Enum):
|
||||||
|
VAL1 = 'val1'
|
||||||
|
VAL2 = 'val2'
|
||||||
|
#
|
||||||
|
class Class1:
|
||||||
|
def __init__(self):
|
||||||
|
# Gracefully handle an exception of our own making
|
||||||
|
try:
|
||||||
|
raise ValueError()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
#
|
||||||
|
class Class2:
|
||||||
|
def __init__(self):
|
||||||
|
# Gracefully handle an exception of Enum's making
|
||||||
|
try:
|
||||||
|
TestEnum('invalid_value')
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# No strong refs here so these are free to die.
|
||||||
|
class_1_ref = weakref.ref(Class1())
|
||||||
|
class_2_ref = weakref.ref(Class2())
|
||||||
|
#
|
||||||
|
# The exception raised by Enum creates a reference loop and thus
|
||||||
|
# Class2 instances will stick around until the next gargage collection
|
||||||
|
# cycle, unlike Class1.
|
||||||
|
self.assertIs(class_1_ref(), None)
|
||||||
|
self.assertIs(class_2_ref(), None)
|
||||||
|
|
||||||
def test_multiple_mixin(self):
|
def test_multiple_mixin(self):
|
||||||
class MaxMixin:
|
class MaxMixin:
|
||||||
@classproperty
|
@classproperty
|
||||||
|
|
@ -2085,7 +2139,7 @@ def test_empty_globals(self):
|
||||||
exec(code, global_ns, local_ls)
|
exec(code, global_ns, local_ls)
|
||||||
|
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
sys.version_info[:2] == (3, 9),
|
python_version == (3, 9),
|
||||||
'private variables are now normal attributes',
|
'private variables are now normal attributes',
|
||||||
)
|
)
|
||||||
def test_warning_for_private_variables(self):
|
def test_warning_for_private_variables(self):
|
||||||
|
|
@ -2390,19 +2444,42 @@ def test_pickle(self):
|
||||||
test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE)
|
test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE)
|
||||||
test_pickle_dump_load(self.assertIs, FlagStooges)
|
test_pickle_dump_load(self.assertIs, FlagStooges)
|
||||||
|
|
||||||
def test_contains(self):
|
@unittest.skipIf(
|
||||||
|
python_version >= (3, 12),
|
||||||
|
'__contains__ now returns True/False for all inputs',
|
||||||
|
)
|
||||||
|
def test_contains_er(self):
|
||||||
Open = self.Open
|
Open = self.Open
|
||||||
Color = self.Color
|
Color = self.Color
|
||||||
self.assertFalse(Color.BLACK in Open)
|
self.assertFalse(Color.BLACK in Open)
|
||||||
self.assertFalse(Open.RO in Color)
|
self.assertFalse(Open.RO in Color)
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
'BLACK' in Color
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
'BLACK' in Color
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
'RO' in Open
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
'RO' in Open
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
1 in Color
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
1 in Color
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
1 in Open
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
1 in Open
|
||||||
|
|
||||||
|
@unittest.skipIf(
|
||||||
|
python_version < (3, 12),
|
||||||
|
'__contains__ only works with enum memmbers before 3.12',
|
||||||
|
)
|
||||||
|
def test_contains_tf(self):
|
||||||
|
Open = self.Open
|
||||||
|
Color = self.Color
|
||||||
|
self.assertFalse(Color.BLACK in Open)
|
||||||
|
self.assertFalse(Open.RO in Color)
|
||||||
|
self.assertFalse('BLACK' in Color)
|
||||||
|
self.assertFalse('RO' in Open)
|
||||||
|
self.assertTrue(1 in Color)
|
||||||
|
self.assertTrue(1 in Open)
|
||||||
|
|
||||||
|
|
||||||
def test_member_contains(self):
|
def test_member_contains(self):
|
||||||
Perm = self.Perm
|
Perm = self.Perm
|
||||||
|
|
@ -2883,7 +2960,11 @@ def test_programatic_function_from_empty_tuple(self):
|
||||||
self.assertEqual(len(lst), len(Thing))
|
self.assertEqual(len(lst), len(Thing))
|
||||||
self.assertEqual(len(Thing), 0, Thing)
|
self.assertEqual(len(Thing), 0, Thing)
|
||||||
|
|
||||||
def test_contains(self):
|
@unittest.skipIf(
|
||||||
|
python_version >= (3, 12),
|
||||||
|
'__contains__ now returns True/False for all inputs',
|
||||||
|
)
|
||||||
|
def test_contains_er(self):
|
||||||
Open = self.Open
|
Open = self.Open
|
||||||
Color = self.Color
|
Color = self.Color
|
||||||
self.assertTrue(Color.GREEN in Color)
|
self.assertTrue(Color.GREEN in Color)
|
||||||
|
|
@ -2891,13 +2972,33 @@ def test_contains(self):
|
||||||
self.assertFalse(Color.GREEN in Open)
|
self.assertFalse(Color.GREEN in Open)
|
||||||
self.assertFalse(Open.RW in Color)
|
self.assertFalse(Open.RW in Color)
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
'GREEN' in Color
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
'GREEN' in Color
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
'RW' in Open
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
'RW' in Open
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
2 in Color
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
2 in Color
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
2 in Open
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
2 in Open
|
||||||
|
|
||||||
|
@unittest.skipIf(
|
||||||
|
python_version < (3, 12),
|
||||||
|
'__contains__ only works with enum memmbers before 3.12',
|
||||||
|
)
|
||||||
|
def test_contains_tf(self):
|
||||||
|
Open = self.Open
|
||||||
|
Color = self.Color
|
||||||
|
self.assertTrue(Color.GREEN in Color)
|
||||||
|
self.assertTrue(Open.RW in Open)
|
||||||
|
self.assertTrue(Color.GREEN in Open)
|
||||||
|
self.assertTrue(Open.RW in Color)
|
||||||
|
self.assertFalse('GREEN' in Color)
|
||||||
|
self.assertFalse('RW' in Open)
|
||||||
|
self.assertTrue(2 in Color)
|
||||||
|
self.assertTrue(2 in Open)
|
||||||
|
|
||||||
def test_member_contains(self):
|
def test_member_contains(self):
|
||||||
Perm = self.Perm
|
Perm = self.Perm
|
||||||
|
|
@ -3267,7 +3368,7 @@ def test_convert(self):
|
||||||
if name[0:2] not in ('CO', '__')],
|
if name[0:2] not in ('CO', '__')],
|
||||||
[], msg='Names other than CONVERT_TEST_* found.')
|
[], msg='Names other than CONVERT_TEST_* found.')
|
||||||
|
|
||||||
@unittest.skipUnless(sys.version_info[:2] == (3, 8),
|
@unittest.skipUnless(python_version == (3, 8),
|
||||||
'_convert was deprecated in 3.8')
|
'_convert was deprecated in 3.8')
|
||||||
def test_convert_warn(self):
|
def test_convert_warn(self):
|
||||||
with self.assertWarns(DeprecationWarning):
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
|
@ -3276,7 +3377,7 @@ def test_convert_warn(self):
|
||||||
('test.test_enum', '__main__')[__name__=='__main__'],
|
('test.test_enum', '__main__')[__name__=='__main__'],
|
||||||
filter=lambda x: x.startswith('CONVERT_TEST_'))
|
filter=lambda x: x.startswith('CONVERT_TEST_'))
|
||||||
|
|
||||||
@unittest.skipUnless(sys.version_info >= (3, 9),
|
@unittest.skipUnless(python_version >= (3, 9),
|
||||||
'_convert was removed in 3.9')
|
'_convert was removed in 3.9')
|
||||||
def test_convert_raise(self):
|
def test_convert_raise(self):
|
||||||
with self.assertRaises(AttributeError):
|
with self.assertRaises(AttributeError):
|
||||||
|
|
@ -3285,6 +3386,33 @@ def test_convert_raise(self):
|
||||||
('test.test_enum', '__main__')[__name__=='__main__'],
|
('test.test_enum', '__main__')[__name__=='__main__'],
|
||||||
filter=lambda x: x.startswith('CONVERT_TEST_'))
|
filter=lambda x: x.startswith('CONVERT_TEST_'))
|
||||||
|
|
||||||
|
class TestHelpers(unittest.TestCase):
|
||||||
|
|
||||||
|
sunder_names = '_bad_', '_good_', '_what_ho_'
|
||||||
|
dunder_names = '__mal__', '__bien__', '__que_que__'
|
||||||
|
private_names = '_MyEnum__private', '_MyEnum__still_private'
|
||||||
|
private_and_sunder_names = '_MyEnum__private_', '_MyEnum__also_private_'
|
||||||
|
random_names = 'okay', '_semi_private', '_weird__', '_MyEnum__'
|
||||||
|
|
||||||
|
def test_sunder(self):
|
||||||
|
for name in self.sunder_names + self.private_and_sunder_names:
|
||||||
|
self.assertTrue(enum._is_sunder(name), '%r is a not sunder name?' % name)
|
||||||
|
for name in self.dunder_names + self.private_names + self.random_names:
|
||||||
|
self.assertFalse(enum._is_sunder(name), '%r is a sunder name?' % name)
|
||||||
|
|
||||||
|
def test_dunder(self):
|
||||||
|
for name in self.dunder_names:
|
||||||
|
self.assertTrue(enum._is_dunder(name), '%r is a not dunder name?' % name)
|
||||||
|
for name in self.sunder_names + self.private_names + self.private_and_sunder_names + self.random_names:
|
||||||
|
self.assertFalse(enum._is_dunder(name), '%r is a dunder name?' % name)
|
||||||
|
|
||||||
|
def test_is_private(self):
|
||||||
|
for name in self.private_names + self.private_and_sunder_names:
|
||||||
|
self.assertTrue(enum._is_private('MyEnum', name), '%r is a not private name?')
|
||||||
|
for name in self.sunder_names + self.dunder_names + self.random_names:
|
||||||
|
self.assertFalse(enum._is_private('MyEnum', name), '%r is a private name?')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue