summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEthan Furman <ethan@stoneleaf.us>2021-10-20 16:07:42 -0700
committerEthan Furman <ethan@stoneleaf.us>2021-10-20 16:07:42 -0700
commit629eb1c9ce67812fc44ed033c9d5f9935f7e9688 (patch)
tree5678e212cbd392c26d0476908aca1391a4554d69
parentf40b230df91f29fa4a84c1d127b4eab56a6eda27 (diff)
downloadcpython-git-enum-lost-fixes.tar.gz
restore fixes lost in enum reversionenum-lost-fixes
-rw-r--r--Doc/library/enum.rst14
-rw-r--r--Lib/enum.py60
-rw-r--r--Lib/test/test_enum.py162
3 files changed, 194 insertions, 42 deletions
diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index e8e49425ab..b354a111a3 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -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
@@ -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
@@ -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>``.
+
diff --git a/Lib/enum.py b/Lib/enum.py
index db79e66903..f5657a6eba 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -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
@@ -392,12 +393,19 @@ class EnumMeta(type):
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
@@ -580,7 +588,7 @@ class EnumMeta(type):
return object, Enum
def _find_data_type(bases):
- data_types = []
+ data_types = set()
for chain in bases:
candidate = None
for base in chain.__mro__:
@@ -588,19 +596,19 @@ class EnumMeta(type):
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
@@ -693,19 +701,25 @@ class Enum(metaclass=EnumMeta):
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 variables that could hold an exception are destroyed
+ exc = None
+ ve_exc = None
def _generate_next_value_(name, start, count, last_values):
"""
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index eb1266b960..03cf3533fc 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -11,6 +11,7 @@ from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test.support import ALWAYS_EQ, check__all__, threading_helper
from datetime import timedelta
+python_version = sys.version_info[:2]
# for pickle tests
try:
@@ -347,17 +348,38 @@ class TestEnum(unittest.TestCase):
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)
@@ -1932,6 +1954,38 @@ class TestEnum(unittest.TestCase):
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
@@ -2085,7 +2139,7 @@ class TestEnum(unittest.TestCase):
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):
@@ -2390,19 +2444,42 @@ class TestFlag(unittest.TestCase):
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
@@ -2883,7 +2960,11 @@ class TestIntFlag(unittest.TestCase):
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)
@@ -2891,13 +2972,33 @@ class TestIntFlag(unittest.TestCase):
self.assertFalse(Color.GREEN in Open)
self.assertFalse(Open.RW in Color)
with self.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
@@ -3267,7 +3368,7 @@ class TestIntEnumConvert(unittest.TestCase):
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):
@@ -3276,7 +3377,7 @@ class TestIntEnumConvert(unittest.TestCase):
('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):
@@ -3285,6 +3386,33 @@ class TestIntEnumConvert(unittest.TestCase):
('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()
+