Skip to content

Commit 9a479c3

Browse files
gh-88123: Implement new Enum __contains__ (GH-93298)
Co-authored-by: Ethan Furman <ethan@stoneleaf.us>
1 parent 6575841 commit 9a479c3

File tree

3 files changed

+79
-112
lines changed

3 files changed

+79
-112
lines changed

Lib/enum.py

+8-18
Original file line numberDiff line numberDiff line change
@@ -799,26 +799,16 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s
799799
boundary=boundary,
800800
)
801801

802-
def __contains__(cls, member):
803-
"""
804-
Return True if member is a member of this enum
805-
raises TypeError if member is not an enum member
802+
def __contains__(cls, value):
803+
"""Return True if `value` is in `cls`.
806804
807-
note: in 3.12 TypeError will no longer be raised, and True will also be
808-
returned if member is the value of a member in this enum
805+
`value` is in `cls` if:
806+
1) `value` is a member of `cls`, or
807+
2) `value` is the value of one of the `cls`'s members.
809808
"""
810-
if not isinstance(member, Enum):
811-
import warnings
812-
warnings.warn(
813-
"in 3.12 __contains__ will no longer raise TypeError, but will return True or\n"
814-
"False depending on whether the value is a member or the value of a member",
815-
DeprecationWarning,
816-
stacklevel=2,
817-
)
818-
raise TypeError(
819-
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
820-
type(member).__qualname__, cls.__class__.__qualname__))
821-
return isinstance(member, cls) and member._name_ in cls._member_map_
809+
if isinstance(value, cls):
810+
return True
811+
return value in cls._value2member_map_ or value in cls._unhashable_values_
822812

823813
def __delattr__(cls, attr):
824814
# nicer error message when someone tries to delete an attribute

Lib/test/test_enum.py

+69-94
Original file line numberDiff line numberDiff line change
@@ -343,45 +343,56 @@ def test_changing_member_fails(self):
343343
with self.assertRaises(AttributeError):
344344
self.MainEnum.second = 'really first'
345345

346-
@unittest.skipIf(
347-
python_version >= (3, 12),
348-
'__contains__ now returns True/False for all inputs',
349-
)
350-
@unittest.expectedFailure
351-
def test_contains_er(self):
346+
def test_contains_tf(self):
352347
MainEnum = self.MainEnum
353-
self.assertIn(MainEnum.third, MainEnum)
354-
with self.assertRaises(TypeError):
355-
with self.assertWarns(DeprecationWarning):
356-
self.source_values[1] in MainEnum
357-
with self.assertRaises(TypeError):
358-
with self.assertWarns(DeprecationWarning):
359-
'first' in MainEnum
348+
self.assertIn(MainEnum.first, MainEnum)
349+
self.assertTrue(self.values[0] in MainEnum)
350+
if type(self) is not TestStrEnum:
351+
self.assertFalse('first' in MainEnum)
360352
val = MainEnum.dupe
361353
self.assertIn(val, MainEnum)
362354
#
363355
class OtherEnum(Enum):
364356
one = auto()
365357
two = auto()
366358
self.assertNotIn(OtherEnum.two, MainEnum)
367-
368-
@unittest.skipIf(
369-
python_version < (3, 12),
370-
'__contains__ works only with enum memmbers before 3.12',
371-
)
372-
@unittest.expectedFailure
373-
def test_contains_tf(self):
359+
#
360+
if MainEnum._member_type_ is object:
361+
# enums without mixed data types will always be False
362+
class NotEqualEnum(self.enum_type):
363+
this = self.source_values[0]
364+
that = self.source_values[1]
365+
self.assertNotIn(NotEqualEnum.this, MainEnum)
366+
self.assertNotIn(NotEqualEnum.that, MainEnum)
367+
else:
368+
# enums with mixed data types may be True
369+
class EqualEnum(self.enum_type):
370+
this = self.source_values[0]
371+
that = self.source_values[1]
372+
self.assertIn(EqualEnum.this, MainEnum)
373+
self.assertIn(EqualEnum.that, MainEnum)
374+
375+
def test_contains_same_name_diff_enum_diff_values(self):
374376
MainEnum = self.MainEnum
375-
self.assertIn(MainEnum.first, MainEnum)
376-
self.assertTrue(self.source_values[0] in MainEnum)
377-
self.assertFalse('first' in MainEnum)
378-
val = MainEnum.dupe
379-
self.assertIn(val, MainEnum)
380377
#
381378
class OtherEnum(Enum):
382-
one = auto()
383-
two = auto()
384-
self.assertNotIn(OtherEnum.two, MainEnum)
379+
first = "brand"
380+
second = "new"
381+
third = "values"
382+
#
383+
self.assertIn(MainEnum.first, MainEnum)
384+
self.assertIn(MainEnum.second, MainEnum)
385+
self.assertIn(MainEnum.third, MainEnum)
386+
self.assertNotIn(MainEnum.first, OtherEnum)
387+
self.assertNotIn(MainEnum.second, OtherEnum)
388+
self.assertNotIn(MainEnum.third, OtherEnum)
389+
#
390+
self.assertIn(OtherEnum.first, OtherEnum)
391+
self.assertIn(OtherEnum.second, OtherEnum)
392+
self.assertIn(OtherEnum.third, OtherEnum)
393+
self.assertNotIn(OtherEnum.first, MainEnum)
394+
self.assertNotIn(OtherEnum.second, MainEnum)
395+
self.assertNotIn(OtherEnum.third, MainEnum)
385396

386397
def test_dir_on_class(self):
387398
TE = self.MainEnum
@@ -1115,6 +1126,28 @@ class Huh(Enum):
11151126
self.assertEqual(Huh.name.name, 'name')
11161127
self.assertEqual(Huh.name.value, 1)
11171128

1129+
def test_contains_name_and_value_overlap(self):
1130+
class IntEnum1(IntEnum):
1131+
X = 1
1132+
class IntEnum2(IntEnum):
1133+
X = 1
1134+
class IntEnum3(IntEnum):
1135+
X = 2
1136+
class IntEnum4(IntEnum):
1137+
Y = 1
1138+
self.assertIn(IntEnum1.X, IntEnum1)
1139+
self.assertIn(IntEnum1.X, IntEnum2)
1140+
self.assertNotIn(IntEnum1.X, IntEnum3)
1141+
self.assertIn(IntEnum1.X, IntEnum4)
1142+
1143+
def test_contains_different_types_same_members(self):
1144+
class IntEnum1(IntEnum):
1145+
X = 1
1146+
class IntFlag1(IntFlag):
1147+
X = 1
1148+
self.assertIn(IntEnum1.X, IntFlag1)
1149+
self.assertIn(IntFlag1.X, IntEnum1)
1150+
11181151
def test_inherited_data_type(self):
11191152
class HexInt(int):
11201153
__qualname__ = 'HexInt'
@@ -2969,41 +3002,15 @@ def test_pickle(self):
29693002
test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE)
29703003
test_pickle_dump_load(self.assertIs, FlagStooges)
29713004

2972-
@unittest.skipIf(
2973-
python_version >= (3, 12),
2974-
'__contains__ now returns True/False for all inputs',
2975-
)
2976-
@unittest.expectedFailure
2977-
def test_contains_er(self):
2978-
Open = self.Open
2979-
Color = self.Color
2980-
self.assertFalse(Color.BLACK in Open)
2981-
self.assertFalse(Open.RO in Color)
2982-
with self.assertRaises(TypeError):
2983-
with self.assertWarns(DeprecationWarning):
2984-
'BLACK' in Color
2985-
with self.assertRaises(TypeError):
2986-
with self.assertWarns(DeprecationWarning):
2987-
'RO' in Open
2988-
with self.assertRaises(TypeError):
2989-
with self.assertWarns(DeprecationWarning):
2990-
1 in Color
2991-
with self.assertRaises(TypeError):
2992-
with self.assertWarns(DeprecationWarning):
2993-
1 in Open
2994-
2995-
@unittest.skipIf(
2996-
python_version < (3, 12),
2997-
'__contains__ only works with enum memmbers before 3.12',
2998-
)
2999-
@unittest.expectedFailure
30003005
def test_contains_tf(self):
30013006
Open = self.Open
30023007
Color = self.Color
30033008
self.assertFalse(Color.BLACK in Open)
30043009
self.assertFalse(Open.RO in Color)
30053010
self.assertFalse('BLACK' in Color)
30063011
self.assertFalse('RO' in Open)
3012+
self.assertTrue(Color.BLACK in Color)
3013+
self.assertTrue(Open.RO in Open)
30073014
self.assertTrue(1 in Color)
30083015
self.assertTrue(1 in Open)
30093016

@@ -3543,43 +3550,11 @@ def test_programatic_function_from_empty_tuple(self):
35433550
self.assertEqual(len(lst), len(Thing))
35443551
self.assertEqual(len(Thing), 0, Thing)
35453552

3546-
@unittest.skipIf(
3547-
python_version >= (3, 12),
3548-
'__contains__ now returns True/False for all inputs',
3549-
)
3550-
@unittest.expectedFailure
3551-
def test_contains_er(self):
3552-
Open = self.Open
3553-
Color = self.Color
3554-
self.assertTrue(Color.GREEN in Color)
3555-
self.assertTrue(Open.RW in Open)
3556-
self.assertFalse(Color.GREEN in Open)
3557-
self.assertFalse(Open.RW in Color)
3558-
with self.assertRaises(TypeError):
3559-
with self.assertWarns(DeprecationWarning):
3560-
'GREEN' in Color
3561-
with self.assertRaises(TypeError):
3562-
with self.assertWarns(DeprecationWarning):
3563-
'RW' in Open
3564-
with self.assertRaises(TypeError):
3565-
with self.assertWarns(DeprecationWarning):
3566-
2 in Color
3567-
with self.assertRaises(TypeError):
3568-
with self.assertWarns(DeprecationWarning):
3569-
2 in Open
3570-
3571-
@unittest.skipIf(
3572-
python_version < (3, 12),
3573-
'__contains__ only works with enum memmbers before 3.12',
3574-
)
3575-
@unittest.expectedFailure
35763553
def test_contains_tf(self):
35773554
Open = self.Open
35783555
Color = self.Color
35793556
self.assertTrue(Color.GREEN in Color)
35803557
self.assertTrue(Open.RW in Open)
3581-
self.assertTrue(Color.GREEN in Open)
3582-
self.assertTrue(Open.RW in Color)
35833558
self.assertFalse('GREEN' in Color)
35843559
self.assertFalse('RW' in Open)
35853560
self.assertTrue(2 in Color)
@@ -4087,12 +4062,12 @@ class Color(enum.Enum)
40874062
| ----------------------------------------------------------------------
40884063
| Methods inherited from enum.EnumType:
40894064
|\x20\x20
4090-
| __contains__(member) from enum.EnumType
4091-
| Return True if member is a member of this enum
4092-
| raises TypeError if member is not an enum member
4093-
|\x20\x20\x20\x20\x20\x20
4094-
| note: in 3.12 TypeError will no longer be raised, and True will also be
4095-
| returned if member is the value of a member in this enum
4065+
| __contains__(value) from enum.EnumType
4066+
| Return True if `value` is in `cls`.
4067+
|
4068+
| `value` is in `cls` if:
4069+
| 1) `value` is a member of `cls`, or
4070+
| 2) `value` is the value of one of the `cls`'s members.
40964071
|\x20\x20
40974072
| __getitem__(name) from enum.EnumType
40984073
| Return the member matching `name`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement Enum __contains__ that returns True or False to replace the
2+
deprecated behaviour that would sometimes raise a TypeError.

0 commit comments

Comments
 (0)