From 9a479c3c1063f434629a1f7553f5a4715d738f30 Mon Sep 17 00:00:00 2001 From: Carl Bordum Hansen Date: Wed, 22 Jun 2022 09:04:04 +0200 Subject: [PATCH] gh-88123: Implement new Enum __contains__ (GH-93298) Co-authored-by: Ethan Furman --- Lib/enum.py | 26 +-- Lib/test/test_enum.py | 163 ++++++++---------- ...2-05-27-22-17-11.gh-issue-88123.mkYl5q.rst | 2 + 3 files changed, 79 insertions(+), 112 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-05-27-22-17-11.gh-issue-88123.mkYl5q.rst diff --git a/Lib/enum.py b/Lib/enum.py index 20fad97e3d1997..decb601496fc9b 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -799,26 +799,16 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s boundary=boundary, ) - def __contains__(cls, member): - """ - Return True if member is a member of this enum - raises TypeError if member is not an enum member + def __contains__(cls, value): + """Return True if `value` is in `cls`. - note: in 3.12 TypeError will no longer be raised, and True will also be - returned if member is the value of a member in this enum + `value` is in `cls` if: + 1) `value` is a member of `cls`, or + 2) `value` is the value of one of the `cls`'s members. """ - 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__)) - return isinstance(member, cls) and member._name_ in cls._member_map_ + if isinstance(value, cls): + return True + return value in cls._value2member_map_ or value in cls._unhashable_values_ def __delattr__(cls, attr): # nicer error message when someone tries to delete an attribute diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 2fadb4eaa1d824..e26ef000ea76c3 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -343,20 +343,12 @@ def test_changing_member_fails(self): with self.assertRaises(AttributeError): self.MainEnum.second = 'really first' - @unittest.skipIf( - python_version >= (3, 12), - '__contains__ now returns True/False for all inputs', - ) - @unittest.expectedFailure - def test_contains_er(self): + def test_contains_tf(self): MainEnum = self.MainEnum - self.assertIn(MainEnum.third, MainEnum) - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - self.source_values[1] in MainEnum - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 'first' in MainEnum + self.assertIn(MainEnum.first, MainEnum) + self.assertTrue(self.values[0] in MainEnum) + if type(self) is not TestStrEnum: + self.assertFalse('first' in MainEnum) val = MainEnum.dupe self.assertIn(val, MainEnum) # @@ -364,24 +356,43 @@ class OtherEnum(Enum): one = auto() two = auto() self.assertNotIn(OtherEnum.two, MainEnum) - - @unittest.skipIf( - python_version < (3, 12), - '__contains__ works only with enum memmbers before 3.12', - ) - @unittest.expectedFailure - def test_contains_tf(self): + # + if MainEnum._member_type_ is object: + # enums without mixed data types will always be False + class NotEqualEnum(self.enum_type): + this = self.source_values[0] + that = self.source_values[1] + self.assertNotIn(NotEqualEnum.this, MainEnum) + self.assertNotIn(NotEqualEnum.that, MainEnum) + else: + # enums with mixed data types may be True + class EqualEnum(self.enum_type): + this = self.source_values[0] + that = self.source_values[1] + self.assertIn(EqualEnum.this, MainEnum) + self.assertIn(EqualEnum.that, MainEnum) + + def test_contains_same_name_diff_enum_diff_values(self): MainEnum = self.MainEnum - self.assertIn(MainEnum.first, MainEnum) - self.assertTrue(self.source_values[0] in MainEnum) - self.assertFalse('first' in MainEnum) - val = MainEnum.dupe - self.assertIn(val, MainEnum) # class OtherEnum(Enum): - one = auto() - two = auto() - self.assertNotIn(OtherEnum.two, MainEnum) + first = "brand" + second = "new" + third = "values" + # + self.assertIn(MainEnum.first, MainEnum) + self.assertIn(MainEnum.second, MainEnum) + self.assertIn(MainEnum.third, MainEnum) + self.assertNotIn(MainEnum.first, OtherEnum) + self.assertNotIn(MainEnum.second, OtherEnum) + self.assertNotIn(MainEnum.third, OtherEnum) + # + self.assertIn(OtherEnum.first, OtherEnum) + self.assertIn(OtherEnum.second, OtherEnum) + self.assertIn(OtherEnum.third, OtherEnum) + self.assertNotIn(OtherEnum.first, MainEnum) + self.assertNotIn(OtherEnum.second, MainEnum) + self.assertNotIn(OtherEnum.third, MainEnum) def test_dir_on_class(self): TE = self.MainEnum @@ -1115,6 +1126,28 @@ class Huh(Enum): self.assertEqual(Huh.name.name, 'name') self.assertEqual(Huh.name.value, 1) + def test_contains_name_and_value_overlap(self): + class IntEnum1(IntEnum): + X = 1 + class IntEnum2(IntEnum): + X = 1 + class IntEnum3(IntEnum): + X = 2 + class IntEnum4(IntEnum): + Y = 1 + self.assertIn(IntEnum1.X, IntEnum1) + self.assertIn(IntEnum1.X, IntEnum2) + self.assertNotIn(IntEnum1.X, IntEnum3) + self.assertIn(IntEnum1.X, IntEnum4) + + def test_contains_different_types_same_members(self): + class IntEnum1(IntEnum): + X = 1 + class IntFlag1(IntFlag): + X = 1 + self.assertIn(IntEnum1.X, IntFlag1) + self.assertIn(IntFlag1.X, IntEnum1) + def test_inherited_data_type(self): class HexInt(int): __qualname__ = 'HexInt' @@ -2969,34 +3002,6 @@ def test_pickle(self): test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE) test_pickle_dump_load(self.assertIs, FlagStooges) - @unittest.skipIf( - python_version >= (3, 12), - '__contains__ now returns True/False for all inputs', - ) - @unittest.expectedFailure - 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): - with self.assertWarns(DeprecationWarning): - 'BLACK' in Color - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 'RO' in Open - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 1 in Color - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 1 in Open - - @unittest.skipIf( - python_version < (3, 12), - '__contains__ only works with enum memmbers before 3.12', - ) - @unittest.expectedFailure def test_contains_tf(self): Open = self.Open Color = self.Color @@ -3004,6 +3009,8 @@ def test_contains_tf(self): self.assertFalse(Open.RO in Color) self.assertFalse('BLACK' in Color) self.assertFalse('RO' in Open) + self.assertTrue(Color.BLACK in Color) + self.assertTrue(Open.RO in Open) self.assertTrue(1 in Color) self.assertTrue(1 in Open) @@ -3543,43 +3550,11 @@ def test_programatic_function_from_empty_tuple(self): self.assertEqual(len(lst), len(Thing)) self.assertEqual(len(Thing), 0, Thing) - @unittest.skipIf( - python_version >= (3, 12), - '__contains__ now returns True/False for all inputs', - ) - @unittest.expectedFailure - 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): - with self.assertWarns(DeprecationWarning): - 'GREEN' in Color - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 'RW' in Open - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 2 in Color - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - 2 in Open - - @unittest.skipIf( - python_version < (3, 12), - '__contains__ only works with enum memmbers before 3.12', - ) - @unittest.expectedFailure 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) @@ -4087,12 +4062,12 @@ class Color(enum.Enum) | ---------------------------------------------------------------------- | Methods inherited from enum.EnumType: |\x20\x20 - | __contains__(member) from enum.EnumType - | Return True if member is a member of this enum - | raises TypeError if member is not an enum member - |\x20\x20\x20\x20\x20\x20 - | note: in 3.12 TypeError will no longer be raised, and True will also be - | returned if member is the value of a member in this enum + | __contains__(value) from enum.EnumType + | Return True if `value` is in `cls`. + | + | `value` is in `cls` if: + | 1) `value` is a member of `cls`, or + | 2) `value` is the value of one of the `cls`'s members. |\x20\x20 | __getitem__(name) from enum.EnumType | Return the member matching `name`. diff --git a/Misc/NEWS.d/next/Library/2022-05-27-22-17-11.gh-issue-88123.mkYl5q.rst b/Misc/NEWS.d/next/Library/2022-05-27-22-17-11.gh-issue-88123.mkYl5q.rst new file mode 100644 index 00000000000000..46bd37a85a7ca1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-05-27-22-17-11.gh-issue-88123.mkYl5q.rst @@ -0,0 +1,2 @@ +Implement Enum __contains__ that returns True or False to replace the +deprecated behaviour that would sometimes raise a TypeError.