8000 [3.10] bpo-44559: [Enum] restore fixes lost in 3.10 -> 3.9 reversion by ethanfurman · Pull Request #29114 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

[3.10] bpo-44559: [Enum] restore fixes lost in 3.10 -> 3.9 reversion #29114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1125,9 +1125,9 @@ and raise an error if the two do not match::
_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
in 3.9 will issue a :exc:`DeprecationWarning`.
in 3.10 will issue a :exc:`DeprecationWarning`.


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

.. note::

This behavior is deprecated and will be removed in 3.12.

.. versionchanged:: 3.5


Expand Down Expand Up @@ -1200,3 +1204,9 @@ all named flags and all named combinations of flags that are in the value::
>>> Color(7) # not named combination
<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 changes: 37 additions & 23 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ def _is_sunder(name):
def _is_private(cls_name, name):
# do not use `re` as `re` imports `enum`
pattern = '_%s__' % (cls_name, )
pat_len = len(pattern)
if (
len(name) >= 5
len(name) > pat_len
and name.startswith(pattern)
and name[len(pattern)] != '_'
and name[pat_len:pat_len+1] != ['_']
and (name[-1] != '_' or name[-2] != '_')
):
return True
Expand Down Expand Up @@ -392,12 +393,19 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s
start=start,
)

def __contains__(cls, member):
if not isinstance(member, Enum):
def __contains__(cls, obj):
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(
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
type(member).__qualname__, cls.__class__.__qualname__))
return isinstance(member, cls) and member._name_ in cls._member_map_
type(obj).__qualname__, cls.__class__.__qualname__))
return isinstance(obj, cls) and obj._name_ in cls._member_map_

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

def _find_data_type(bases):
data_types = []
data_types = set()
for chain in bases:
candidate = None
for base in chain.__mro__:
if base is object:
continue
elif issubclass(base, Enum):
if base._member_type_ is not object:
data_types.append(base._member_type_)
data_types.add(base._member_type_)
break
elif '__new__' in base.__dict__:
if issubclass(base, Enum):
continue
data_types.append(candidate or base)
data_types.add(candidate or base)
break
else:
candidate = candidate or base
if len(data_types) > 1:
raise TypeError('%r: too many data types: %r' % (class_name, data_types))
elif data_types:
return data_types[0]
return data_types.pop()
else:
return None

Expand Down Expand Up @@ -693,19 +701,25 @@ def __new__(cls, value):
except Exception as e:
exc = e
result = None
if isinstance(result, cls):
return result
else:
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
if result is None and exc is None:
raise ve_exc
elif exc is None:
exc = TypeError(
'error in %s._missing_: returned %r instead of None or a valid member'
% (cls.__name__, result)
)
exc.__context__ = ve_exc
raise exc
try:
if isinstance(result, cls):
return result
else:
ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
if result is None and exc is None:
raise ve_exc
elif exc is None:
exc = TypeError(
'error in %s._missing_: returned %r instead of None or a valid member'
% (cls.__name__, result)
)
if not isinstance(exc, ValueError):
exc.__context__ = ve_exc
raise exc
finally:
# ensure all varia 8000 bles that could hold an exception are destroyed
exc = None
ve_exc = None

def _generate_next_value_(name, start, count, last_values):
"""
Expand Down
162 changes: 145 additions & 17 deletions Lib/test/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from test.support import ALWAYS_EQ, check__all__, threading_helper
from datetime import timedelta

python_version = sys.version_info[:2]

# for pickle tests
try:
Expand Down Expand Up @@ -347,17 +348,38 @@ class IntLogic(int, Enum):
self.assertTrue(IntLogic.true)
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
self.assertIn(Season.AUTUMN, Season)
with self.assertRaises(TypeError):
3 in Season
with self.assertWarns(DeprecationWarning):
3 in Season
with self.assertRaises(TypeError):
'AUTUMN' in Season

with self.assertWarns(DeprecationWarning):
'AUTUMN' in Season
val = Season(3)
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):
one = 1; two = 2
self.assertNotIn(OtherEnum.two, Season)
Expand Down Expand Up @@ -1932,6 +1954,38 @@ def _missing_(cls, item):
else:
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):
class MaxMixin:
@classproperty
Expand Down Expand Up @@ -2085,7 +2139,7 @@ def test_empty_globals(self):
exec(code, global_ns, local_ls)

@unittest.skipUnless(
sys.version_info[:2] == (3, 9),
python_version == (3, 9),
'private variables are now normal attributes',
)
def test_warning_for_private_variables(self):
Expand Down Expand Up @@ -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)

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
Color = self.Color
self.assertFalse(Color.BLACK in Open)
self.assertFalse(Open.RO in Color)
with self.assertRaises(TypeError):
'BLACK' in Color
with self.assertWarns(DeprecationWarning):
'BLACK' in Color
with self.assertRaises(TypeError):
'RO' in Open
with self.assertWarns(DeprecationWarning):
'RO' in Open
with self.assertRaises(TypeError):
1 in Color
with self.assertWarns(DeprecationWarning):
1 in Color
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):
Perm = self.Perm
Expand Down Expand Up @@ -2883,21 +2960,45 @@ def test_programatic_function_from_empty_tuple(self):
self.assertEqual(len(lst), len(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
Color = self.Color
self.assertTrue(Color.GREEN in Color)
self.assertTrue(Open.RW in Open)
self.assertFalse(Color.GREEN in Open)
self.assertFalse(Open.RW in Color)
with self. F438 assertRaises(TypeError):
'GREEN' in Color
with self.assertWarns(DeprecationWarning):
'GREEN' in Color
with self.assertRaises(TypeError):
'RW' in Open
with self.assertWarns(DeprecationWarning):
'RW' in Open
with self.assertRaises(TypeError):
2 in Color
with self.assertWarns(DeprecationWarning):
2 in Color
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):
Perm = self.Perm
Expand Down Expand Up @@ -3267,7 +3368,7 @@ def test_convert(self):
if name[0:2] not in ('CO', '__')],
[], 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')
def test_convert_warn(self):
with self.assertWarns(DeprecationWarning):
Expand All @@ -3276,7 +3377,7 @@ def test_convert_warn(self):
('test.test_enum', '__main__')[__name__=='__main__'],
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')
def test_convert_raise(self):
with self.assertRaises(AttributeError):
Expand All @@ -3285,6 +3386,33 @@ def test_convert_raise(self):
('test.test_enum', '__main__')[__name__=='__main__'],
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__':
unittest.main()

0