From 3c45b5744fc80646fe0ecf16af21a09495f88868 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 5 Apr 2018 13:37:26 -0700 Subject: [PATCH 1/4] bpo-33217: deprecate non-Enum lookups in Enums Lookups such as `1 in Color` and `2 in SomeFlag()` will raise TypeError in 3.8+. --- Doc/library/enum.rst | 2 +- Lib/enum.py | 12 +++- Lib/test/test_enum.py | 60 ++++++++++++++++++- .../2018-04-05-13-36-09.bpo-33217.FfOKDI.rst | 2 + 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-04-05-13-36-09.bpo-33217.FfOKDI.rst diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index fc65a3d078f19c..787670c6ddad4e 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -976,7 +976,7 @@ Enum Classes The :class:`EnumMeta` metaclass is responsible for providing the :meth:`__contains__`, :meth:`__dir__`, :meth:`__iter__` and other methods that allow one to do things with an :class:`Enum` class that fail on a typical -class, such as `list(Color)` or `some_var in Color`. :class:`EnumMeta` is +class, such as `list(Color)` or `some_enum_var in Color`. :class:`EnumMeta` is responsible for ensuring that various other methods on the final :class:`Enum` class are correct (such as :meth:`__new__`, :meth:`__getnewargs__`, :meth:`__str__` and :meth:`__repr__`). diff --git a/Lib/enum.py b/Lib/enum.py index e5fe6f3b94a1d0..311afba5b0204c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,5 +1,6 @@ import sys from types import MappingProxyType, DynamicClassAttribute +import warnings # try _collections first to reduce startup cost try: @@ -309,6 +310,11 @@ def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, s return cls._create_(value, names, module=module, qualname=qualname, type=type, start=start) def __contains__(cls, member): + if not isinstance(member, Enum): + warnings.warn( + "using non-Enums in containment checks will raise " + "TypeError in 3.8+", + DeprecationWarning, 2) return isinstance(member, cls) and member._name_ in cls._member_map_ def __delattr__(cls, attr): @@ -713,7 +719,11 @@ def _create_pseudo_member_(cls, value): def __contains__(self, other): if not isinstance(other, self.__class__): - return NotImplemented + warnings.warn( + "using non-Flags in containment checks will raise " + "TypeError in 3.8+", + DeprecationWarning, 2) + return False return other._value_ & self._value_ == other._value_ def __repr__(self): diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 97559712b1dc2c..ef2d1daaf9420d 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -325,7 +325,10 @@ class IntLogic(int, Enum): def test_contains(self): Season = self.Season self.assertIn(Season.AUTUMN, Season) - self.assertNotIn(3, Season) + with self.assertWarns(DeprecationWarning): + self.assertNotIn(3, Season) + with self.assertWarns(DeprecationWarning): + self.assertNotIn('AUTUMN', Season) val = Season(3) self.assertIn(val, Season) @@ -334,6 +337,11 @@ class OtherEnum(Enum): one = 1; two = 2 self.assertNotIn(OtherEnum.two, Season) + def test_member_contains(self): + self.assertRaises(TypeError, lambda: 'test' in self.Season.AUTUMN) + self.assertRaises(TypeError, lambda: 3 in self.Season.AUTUMN) + self.assertRaises(TypeError, lambda: 'AUTUMN' in self.Season.AUTUMN) + def test_comparisons(self): Season = self.Season with self.assertRaises(TypeError): @@ -1745,6 +1753,13 @@ class TestFlag(unittest.TestCase): class Perm(Flag): R, W, X = 4, 2, 1 + class Color(Flag): + BLACK = 0 + RED = 1 + GREEN = 2 + BLUE = 4 + PURPLE = RED|BLUE + class Open(Flag): RO = 0 WO = 1 @@ -1954,7 +1969,21 @@ def test_pickle(self): test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE) test_pickle_dump_load(self.assertIs, FlagStooges) - def test_containment(self): + def test_contains(self): + Open = self.Open + Color = self.Color + self.assertFalse(Color.BLACK in Open) + self.assertFalse(Open.RO in Color) + with self.assertWarns(DeprecationWarning): + self.assertFalse('BLACK' in Color) + with self.assertWarns(DeprecationWarning): + self.assertFalse('RO' in Open) + with self.assertWarns(DeprecationWarning): + self.assertFalse(1 in Color) + with self.assertWarns(DeprecationWarning): + self.assertFalse(1 in Open) + + def test_member_contains(self): Perm = self.Perm R, W, X = Perm RW = R | W @@ -2065,6 +2094,13 @@ class Perm(IntFlag): W = 1 << 1 R = 1 << 2 + class Color(IntFlag): + BLACK = 0 + RED = 1 + GREEN = 2 + BLUE = 4 + PURPLE = RED|BLUE + class Open(IntFlag): RO = 0 WO = 1 @@ -2340,7 +2376,23 @@ def test_programatic_function_from_empty_tuple(self): self.assertEqual(len(lst), len(Thing)) self.assertEqual(len(Thing), 0, Thing) - def test_containment(self): + def test_contains(self): + Color = self.Color + Open = self.Open + 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.assertWarns(DeprecationWarning): + self.assertFalse('GREEN' in Color) + with self.assertWarns(DeprecationWarning): + self.assertFalse('RW' in Open) + with self.assertWarns(DeprecationWarning): + self.assertFalse(2 in Color) + with self.assertWarns(DeprecationWarning): + self.assertFalse(2 in Open) + + def test_member_contains(self): Perm = self.Perm R, W, X = Perm RW = R | W @@ -2359,6 +2411,8 @@ def test_containment(self): self.assertFalse(R in WX) self.assertFalse(W in RX) self.assertFalse(X in RW) + with self.assertWarns(DeprecationWarning): + self.assertFalse('swallow' in RW) def test_bool(self): Perm = self.Perm diff --git a/Misc/NEWS.d/next/Library/2018-04-05-13-36-09.bpo-33217.FfOKDI.rst b/Misc/NEWS.d/next/Library/2018-04-05-13-36-09.bpo-33217.FfOKDI.rst new file mode 100644 index 00000000000000..8dfc9e5a15c0e3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-04-05-13-36-09.bpo-33217.FfOKDI.rst @@ -0,0 +1,2 @@ +Deprecate looking up non-Enum objects in Enum classes and Enum members (will +raise :exc:`TypeError` in 3.8+). From 438085e8f463c3c5629e9058c9bcd74c2fe01fc4 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 10 Apr 2018 12:24:56 -0700 Subject: [PATCH 2/4] move warnings import to just-before use --- Lib/enum.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 311afba5b0204c..c28c7a71774e61 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,6 +1,5 @@ import sys from types import MappingProxyType, DynamicClassAttribute -import warnings # try _collections first to reduce startup cost try: @@ -311,9 +310,10 @@ 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( "using non-Enums in containment checks will raise " - "TypeError in 3.8+", + "TypeError in 3.8", DeprecationWarning, 2) return isinstance(member, cls) and member._name_ in cls._member_map_ @@ -719,9 +719,10 @@ def _create_pseudo_member_(cls, value): def __contains__(self, other): if not isinstance(other, self.__class__): + import warnings warnings.warn( "using non-Flags in containment checks will raise " - "TypeError in 3.8+", + "TypeError in 3.8", DeprecationWarning, 2) return False return other._value_ & self._value_ == other._value_ From 430596061138aec3a7d76064fa4fa2372c8f4a6b Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 10 Apr 2018 12:49:49 -0700 Subject: [PATCH 3/4] Add "Python" to warning. --- Lib/enum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index c28c7a71774e61..8c8409bd4efd29 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -313,7 +313,7 @@ def __contains__(cls, member): import warnings warnings.warn( "using non-Enums in containment checks will raise " - "TypeError in 3.8", + "TypeError in Python 3.8", DeprecationWarning, 2) return isinstance(member, cls) and member._name_ in cls._member_map_ @@ -722,7 +722,7 @@ def __contains__(self, other): import warnings warnings.warn( "using non-Flags in containment checks will raise " - "TypeError in 3.8", + "TypeError in Python 3.8", DeprecationWarning, 2) return False return other._value_ & self._value_ == other._value_ From ae1d7518507ed0c90a554866cb0506dd428772d0 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 11 Apr 2018 15:51:21 -0700 Subject: [PATCH 4/4] add entry to WHATSNEW::Deprecated --- Doc/whatsnew/3.7.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 1f524884adaed4..4c6dd774d8def4 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -1041,6 +1041,12 @@ Deprecated :meth:`ssl.SSLContext.wrap_socket` instead. (Contributed by Christian Heimes in :issue:`28124`.) +- In Python 3.8, attempting to check for non-Enum objects in :class:`Enum` + classes will raise a :exc:`TypeError` (e.g. ``1 in Color``); similarly, + attempting to check for non-Flag objects in a :class:`Flag` member will + raise :exc:`TypeError` (e.g. ``1 in Perm.RW``); currently, both operations + return :const:`False` instead. + Windows Only ------------