Skip to content
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

bpo-43957: [Enum] deprecate TypeError from containment checks #25670

Merged
merged 1 commit into from
Apr 27, 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
6 changes: 6 additions & 0 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ Data Types
>>> some_var in Color
True

.. note::

In Python 3.12 it will be possible to check for member values and not
just members; until then, a ``TypeError`` will be raised if a
non-Enum-member is used in a containment check.

.. method:: EnumType.__dir__(cls)

Returns ``['__class__', '__doc__', '__members__', '__module__']`` and the
Expand Down
29 changes: 20 additions & 9 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ def __set_name__(self, enum_class, member_name):
# linear.
enum_class._value2member_map_.setdefault(value, enum_member)
except TypeError:
pass
# keep track of the value in a list so containment checks are quick
enum_class._unhashable_values_.append(value)


class _EnumDict(dict):
Expand Down Expand Up @@ -440,6 +441,7 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k
classdict['_member_names_'] = []
classdict['_member_map_'] = {}
classdict['_value2member_map_'] = {}
classdict['_unhashable_values_'] = []
classdict['_member_type_'] = member_type
#
# Flag structures (will be removed if final class is not a Flag
Expand Down Expand Up @@ -622,6 +624,13 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s

def __contains__(cls, member):
if not isinstance(member, Enum):
import warnings
warnings.warn(
"in 3.12 __contains__ will no longer raise TypeError, but will return True or\n"
"False depending on whether the value is a member or the value of a member",
DeprecationWarning,
stacklevel=2,
)
raise TypeError(
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
type(member).__qualname__, cls.__class__.__qualname__))
Expand Down Expand Up @@ -1005,14 +1014,15 @@ def __format__(self, format_spec):
val = str(self)
# mix-in branch
else:
import warnings
warnings.warn(
"in 3.12 format() will use the enum member, not the enum member's value;\n"
"use a format specifier, such as :d for an IntEnum member, to maintain"
"the current display",
DeprecationWarning,
stacklevel=2,
)
if not format_spec or format_spec in ('{}','{:}'):
import warnings
warnings.warn(
"in 3.12 format() will use the enum member, not the enum member's value;\n"
"use a format specifier, such as :d for an IntEnum member, to maintain"
"the current display",
DeprecationWarning,
stacklevel=2,
)
cls = self._member_type_
val = self._value_
return cls.__format__(val, format_spec)
Expand Down Expand Up @@ -1434,6 +1444,7 @@ def convert_class(cls):
body['_member_names_'] = member_names = []
body['_member_map_'] = member_map = {}
body['_value2member_map_'] = value2member_map = {}
body['_unhashable_values_'] = []
body['_member_type_'] = member_type = etype._member_type_
if issubclass(etype, Flag):
body['_boundary_'] = boundary or etype._boundary_
Expand Down
143 changes: 117 additions & 26 deletions Lib/test/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from test.support import threading_helper
from datetime import timedelta

python_version = sys.version_info[:2]

def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(enum))
if os.path.exists('Doc/library/enum.rst'):
Expand Down Expand Up @@ -352,17 +354,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 @@ -528,16 +551,28 @@ def __format__(self, spec):
self.assertEqual(str(TestFloat.one), 'one')
self.assertEqual('{}'.format(TestFloat.one), 'TestFloat success!')

@unittest.skipUnless(
sys.version_info[:2] < (3, 12),
@unittest.skipIf(
python_version < (3, 12),
'mixin-format is still using member.value',
)
def test_mixin_format_warning(self):
with self.assertWarns(DeprecationWarning):
self.assertEqual(f'{self.Grades.B}', 'Grades.B')

@unittest.skipIf(
python_version >= (3, 12),
'mixin-format now uses member instead of member.value',
)
def test_mixin_format_warning(self):
with self.assertWarns(DeprecationWarning):
self.assertEqual(f'{self.Grades.B}', '4')

def assertFormatIsValue(self, spec, member):
self.assertEqual(spec.format(member), spec.format(member.value))
if python_version < (3, 12) and (not spec or spec in ('{}','{:}')):
with self.assertWarns(DeprecationWarning):
self.assertEqual(spec.format(member), spec.format(member.value))
else:
self.assertEqual(spec.format(member), spec.format(member.value))

def test_format_enum_date(self):
Holiday = self.Holiday
Expand Down Expand Up @@ -2202,7 +2237,7 @@ def __repr__(self):
description = 'Bn$', 3

@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 All @@ -2225,7 +2260,7 @@ class Private(Enum):
self.assertEqual(Private._Private__major_, 'Hoolihan')

@unittest.skipUnless(
sys.version_info[:2] < (3, 12),
python_version < (3, 12),
'member-member access now raises an exception',
)
def test_warning_for_member_from_member_access(self):
Expand All @@ -2237,7 +2272,7 @@ class Di(Enum):
self.assertIs(Di.NO, nope)

@unittest.skipUnless(
sys.version_info[:2] >= (3, 12),
python_version >= (3, 12),
'member-member access currently issues a warning',
)
def test_exception_for_member_from_member_access(self):
Expand Down Expand Up @@ -2617,19 +2652,41 @@ 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 @@ -2954,10 +3011,15 @@ def test_repr(self):
self.assertEqual(repr(~(Open.WO | Open.CE)), 'Open.RW')
self.assertEqual(repr(Open(~4)), '-5')

@unittest.skipUnless(
python_version < (3, 12),
'mixin-format now uses member instead of member.value',
)
def test_format(self):
Perm = self.Perm
self.assertEqual(format(Perm.R, ''), '4')
self.assertEqual(format(Perm.R | Perm.X, ''), '5')
with self.assertWarns(DeprecationWarning):
Perm = self.Perm
self.assertEqual(format(Perm.R, ''), '4')
self.assertEqual(format(Perm.R | Perm.X, ''), '5')

def test_or(self):
Perm = self.Perm
Expand Down Expand Up @@ -3189,21 +3251,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.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 @@ -3685,7 +3771,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 @@ -3694,7 +3780,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 @@ -3703,6 +3789,10 @@ def test_convert_raise(self):
('test.test_enum', '__main__')[__name__=='__main__'],
filter=lambda x: x.startswith('CONVERT_TEST_'))

@unittest.skipUnless(
python_version < (3, 12),
'mixin-format now uses member instead of member.value',
)
def test_convert_repr_and_str(self):
module = ('test.test_enum', '__main__')[__name__=='__main__']
test_type = enum.IntEnum._convert_(
Expand All @@ -3711,7 +3801,8 @@ def test_convert_repr_and_str(self):
filter=lambda x: x.startswith('CONVERT_STRING_TEST_'))
self.assertEqual(repr(test_type.CONVERT_STRING_TEST_NAME_A), '%s.CONVERT_STRING_TEST_NAME_A' % module)
self.assertEqual(str(test_type.CONVERT_STRING_TEST_NAME_A), 'CONVERT_STRING_TEST_NAME_A')
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
with self.assertWarns(DeprecationWarning):
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')

# global names for StrEnum._convert_ test
CONVERT_STR_TEST_2 = 'goodbye'
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ def cycle_handlers():
# race condition, check it.
self.assertIsInstance(cm.unraisable.exc_value, OSError)
self.assertIn(
f"Signal {signum} ignored due to race condition",
f"Signal {signum:d} ignored due to race condition",
str(cm.unraisable.exc_value))
ignored = True

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Enum] Deprecate ``TypeError`` when non-member is used in a containment
check; In 3.12 ``True`` or ``False`` will be returned instead, and
containment will return ``True`` if the value is either a member of that
enum or one of its members' value.