From 650b70ab5076fb38334e0eb6dac6d91bd910a5a7 Mon Sep 17 00:00:00 2001 From: Carl Bordum Hansen Date: Fri, 27 May 2022 22:17:17 +0200 Subject: [PATCH 1/2] gh-88123: Implement new Enum __contains__ --- Lib/enum.py | 27 ++-- Lib/test/test_enum.py | 151 +++++++----------- ...2-05-27-22-17-11.gh-issue-88123.mkYl5q.rst | 2 + 3 files changed, 70 insertions(+), 110 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..835e77732a2cb2 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -800,25 +800,18 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s ) def __contains__(cls, member): - """ - Return True if member is a member of this enum - raises TypeError if member is not an enum member + """Return True if `member` is in `cls`. + + `member` is in `cls` iff: + 1) `member` is a proper member of the `cls` enum, or + 2) `member` is the value of a member of the `cls` enum. - 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 + Beware that 2) can lead to some confusion if members of different + enums have the same value. """ - 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(member, cls): + return True + return member in cls._value2member_map_ or member 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..a1c048b5a28414 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -343,20 +343,15 @@ 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) + if type(self) is TestMinimalDate or type(self) is TestMixedDate: + self.assertTrue(date(*self.source_values[0]) in MainEnum) + else: + self.assertTrue(self.source_values[0] in MainEnum) + if type(self) is not TestStrEnum: + self.assertFalse('first' in MainEnum) val = MainEnum.dupe self.assertIn(val, MainEnum) # @@ -365,23 +360,26 @@ class OtherEnum(Enum): 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): + 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 +1113,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 +2989,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 +2996,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 +3537,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) @@ -4088,11 +4050,14 @@ 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 + | Return True if `member` is in `cls`. + | + | `member` is in `cls` iff: + | 1) `member` is a proper member of the `cls` enum, or + | 2) `member` is the value of a member of the `cls` enum. + | + | Beware that 2) can lead to some confusion if members of different + | enums have the same value. |\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. From 4560269580dcc31425ad73efd71de3cda137dba2 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 21 Jun 2022 21:40:48 -0700 Subject: [PATCH 2/2] minor updates --- Lib/enum.py | 17 +++++++---------- Lib/test/test_enum.py | 38 ++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 835e77732a2cb2..decb601496fc9b 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -799,19 +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 in `cls`. + def __contains__(cls, value): + """Return True if `value` is in `cls`. - `member` is in `cls` iff: - 1) `member` is a proper member of the `cls` enum, or - 2) `member` is the value of a member of the `cls` enum. - - Beware that 2) can lead to some confusion if members of different - enums have the same value. + `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 isinstance(member, cls): + if isinstance(value, cls): return True - return member in cls._value2member_map_ or member in cls._unhashable_values_ + 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 a1c048b5a28414..e26ef000ea76c3 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -346,10 +346,7 @@ def test_changing_member_fails(self): def test_contains_tf(self): MainEnum = self.MainEnum self.assertIn(MainEnum.first, MainEnum) - if type(self) is TestMinimalDate or type(self) is TestMixedDate: - self.assertTrue(date(*self.source_values[0]) in MainEnum) - else: - self.assertTrue(self.source_values[0] in MainEnum) + self.assertTrue(self.values[0] in MainEnum) if type(self) is not TestStrEnum: self.assertFalse('first' in MainEnum) val = MainEnum.dupe @@ -359,21 +356,37 @@ class OtherEnum(Enum): one = auto() two = auto() self.assertNotIn(OtherEnum.two, MainEnum) + # + 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 + # class OtherEnum(Enum): 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) @@ -4049,15 +4062,12 @@ class Color(enum.Enum) | ---------------------------------------------------------------------- | Methods inherited from enum.EnumType: |\x20\x20 - | __contains__(member) from enum.EnumType - | Return True if `member` is in `cls`. - | - | `member` is in `cls` iff: - | 1) `member` is a proper member of the `cls` enum, or - | 2) `member` is the value of a member of the `cls` enum. + | __contains__(value) from enum.EnumType + | Return True if `value` is in `cls`. | - | Beware that 2) can lead to some confusion if members of different - | enums have the same value. + | `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`.