From 0fb9fadd3b3e9e3698647e0b92d49b0b7aacd979 Mon Sep 17 00:00:00 2001 From: orlnub123 Date: Wed, 12 Sep 2018 20:28:53 +0300 Subject: [PATCH] bpo-34282: Fix Enum._convert shadowing members named _convert (GH-8568) * Fix enum members getting shadowed by parent attributes * Move Enum._convert to EnumMeta._convert_ * Deprecate _convert --- Lib/enum.py | 83 ++++++++++--------- Lib/signal.py | 6 +- Lib/socket.py | 8 +- Lib/ssl.py | 12 +-- Lib/test/test_enum.py | 40 ++++++++- Misc/ACKS | 1 + .../2018-08-31-06-28-03.bpo-34282.ztyXH8.rst | 2 + 7 files changed, 99 insertions(+), 53 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-08-31-06-28-03.bpo-34282.ztyXH8.rst diff --git a/Lib/enum.py b/Lib/enum.py index 9d1aef372c1..0839671cca0 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -165,9 +165,11 @@ class EnumMeta(type): enum_class._member_map_ = {} # name->value map enum_class._member_type_ = member_type - # save attributes from super classes so we know if we can take - # the shortcut of storing members in the class dict - base_attributes = {a for b in enum_class.mro() for a in b.__dict__} + # save DynamicClassAttribute attributes from super classes so we know + # if we can take the shortcut of storing members in the class dict + dynamic_attributes = {k for c in enum_class.mro() + for k, v in c.__dict__.items() + if isinstance(v, DynamicClassAttribute)} # Reverse value->name map for hashable values. enum_class._value2member_map_ = {} @@ -227,7 +229,7 @@ class EnumMeta(type): enum_class._member_names_.append(member_name) # performance boost for any member that would not shadow # a DynamicClassAttribute - if member_name not in base_attributes: + if member_name not in dynamic_attributes: setattr(enum_class, member_name, enum_member) # now add to _member_map_ enum_class._member_map_[member_name] = enum_member @@ -428,6 +430,45 @@ class EnumMeta(type): return enum_class + def _convert_(cls, name, module, filter, source=None): + """ + Create a new Enum subclass that replaces a collection of global constants + """ + # convert all constants from source (or module) that pass filter() to + # a new Enum called name, and export the enum and its members back to + # module; + # also, replace the __reduce_ex__ method so unpickling works in + # previous Python versions + module_globals = vars(sys.modules[module]) + if source: + source = vars(source) + else: + source = module_globals + # _value2member_map_ is populated in the same order every time + # for a consistent reverse mapping of number to name when there + # are multiple names for the same number. + members = [ + (name, value) + for name, value in source.items() + if filter(name)] + try: + # sort by value + members.sort(key=lambda t: (t[1], t[0])) + except TypeError: + # unless some values aren't comparable, in which case sort by name + members.sort(key=lambda t: t[0]) + cls = cls(name, members, module=module) + cls.__reduce_ex__ = _reduce_ex_by_name + module_globals.update(cls.__members__) + module_globals[name] = cls + return cls + + def _convert(cls, *args, **kwargs): + import warnings + warnings.warn("_convert is deprecated and will be removed in 3.9, use " + "_convert_ instead.", DeprecationWarning, stacklevel=2) + return cls._convert_(*args, **kwargs) + @staticmethod def _get_mixins_(bases): """Returns the type for creating enum members, and the first inherited @@ -613,40 +654,6 @@ class Enum(metaclass=EnumMeta): """The value of the Enum member.""" return self._value_ - @classmethod - def _convert(cls, name, module, filter, source=None): - """ - Create a new Enum subclass that replaces a collection of global constants - """ - # convert all constants from source (or module) that pass filter() to - # a new Enum called name, and export the enum and its members back to - # module; - # also, replace the __reduce_ex__ method so unpickling works in - # previous Python versions - module_globals = vars(sys.modules[module]) - if source: - source = vars(source) - else: - source = module_globals - # _value2member_map_ is populated in the same order every time - # for a consistent reverse mapping of number to name when there - # are multiple names for the same number. - members = [ - (name, value) - for name, value in source.items() - if filter(name)] - try: - # sort by value - members.sort(key=lambda t: (t[1], t[0])) - except TypeError: - # unless some values aren't comparable, in which case sort by name - members.sort(key=lambda t: t[0]) - cls = cls(name, members, module=module) - cls.__reduce_ex__ = _reduce_ex_by_name - module_globals.update(cls.__members__) - module_globals[name] = cls - return cls - class IntEnum(int, Enum): """Enum where members are also (and must be) ints""" diff --git a/Lib/signal.py b/Lib/signal.py index 826b62cf596..d4a6d6fe2ad 100644 --- a/Lib/signal.py +++ b/Lib/signal.py @@ -5,19 +5,19 @@ from enum import IntEnum as _IntEnum _globals = globals() -_IntEnum._convert( +_IntEnum._convert_( 'Signals', __name__, lambda name: name.isupper() and (name.startswith('SIG') and not name.startswith('SIG_')) or name.startswith('CTRL_')) -_IntEnum._convert( +_IntEnum._convert_( 'Handlers', __name__, lambda name: name in ('SIG_DFL', 'SIG_IGN')) if 'pthread_sigmask' in _globals: - _IntEnum._convert( + _IntEnum._convert_( 'Sigmasks', __name__, lambda name: name in ('SIG_BLOCK', 'SIG_UNBLOCK', 'SIG_SETMASK')) diff --git a/Lib/socket.py b/Lib/socket.py index cfa605a22ad..385844b5853 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -70,22 +70,22 @@ __all__.extend(os._get_exports_list(_socket)) # in this module understands the enums and translates them back from integers # where needed (e.g. .family property of a socket object). -IntEnum._convert( +IntEnum._convert_( 'AddressFamily', __name__, lambda C: C.isupper() and C.startswith('AF_')) -IntEnum._convert( +IntEnum._convert_( 'SocketKind', __name__, lambda C: C.isupper() and C.startswith('SOCK_')) -IntFlag._convert( +IntFlag._convert_( 'MsgFlag', __name__, lambda C: C.isupper() and C.startswith('MSG_')) -IntFlag._convert( +IntFlag._convert_( 'AddressInfo', __name__, lambda C: C.isupper() and C.startswith('AI_')) diff --git a/Lib/ssl.py b/Lib/ssl.py index fdd16157443..fa7c152ade9 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -119,32 +119,32 @@ from _ssl import ( from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION -_IntEnum._convert( +_IntEnum._convert_( '_SSLMethod', __name__, lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23', source=_ssl) -_IntFlag._convert( +_IntFlag._convert_( 'Options', __name__, lambda name: name.startswith('OP_'), source=_ssl) -_IntEnum._convert( +_IntEnum._convert_( 'AlertDescription', __name__, lambda name: name.startswith('ALERT_DESCRIPTION_'), source=_ssl) -_IntEnum._convert( +_IntEnum._convert_( 'SSLErrorNumber', __name__, lambda name: name.startswith('SSL_ERROR_'), source=_ssl) -_IntFlag._convert( +_IntFlag._convert_( 'VerifyFlags', __name__, lambda name: name.startswith('VERIFY_'), source=_ssl) -_IntEnum._convert( +_IntEnum._convert_( 'VerifyMode', __name__, lambda name: name.startswith('CERT_'), source=_ssl) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 68483e65425..c04d03f3752 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1,6 +1,7 @@ import enum import inspect import pydoc +import sys import unittest import threading from collections import OrderedDict @@ -1511,6 +1512,23 @@ class TestEnum(unittest.TestCase): yellow = 6 self.assertEqual(MoreColor.magenta.hex(), '5 hexlified!') + def test_subclass_duplicate_name(self): + class Base(Enum): + def test(self): + pass + class Test(Base): + test = 1 + self.assertIs(type(Test.test), Test) + + def test_subclass_duplicate_name_dynamic(self): + from types import DynamicClassAttribute + class Base(Enum): + @DynamicClassAttribute + def test(self): + return 'dynamic' + class Test(Base): + test = 1 + self.assertEqual(Test.test.test, 'dynamic') def test_no_duplicates(self): class UniqueEnum(Enum): @@ -2668,7 +2686,7 @@ CONVERT_TEST_NAME_F = 5 class TestIntEnumConvert(unittest.TestCase): def test_convert_value_lookup_priority(self): - test_type = enum.IntEnum._convert( + test_type = enum.IntEnum._convert_( 'UnittestConvert', ('test.test_enum', '__main__')[__name__=='__main__'], filter=lambda x: x.startswith('CONVERT_TEST_')) @@ -2678,7 +2696,7 @@ class TestIntEnumConvert(unittest.TestCase): self.assertEqual(test_type(5).name, 'CONVERT_TEST_NAME_A') def test_convert(self): - test_type = enum.IntEnum._convert( + test_type = enum.IntEnum._convert_( 'UnittestConvert', ('test.test_enum', '__main__')[__name__=='__main__'], filter=lambda x: x.startswith('CONVERT_TEST_')) @@ -2694,6 +2712,24 @@ class TestIntEnumConvert(unittest.TestCase): if name[0:2] not in ('CO', '__')], [], msg='Names other than CONVERT_TEST_* found.') + @unittest.skipUnless(sys.version_info[:2] == (3, 8), + '_convert was deprecated in 3.8') + def test_convert_warn(self): + with self.assertWarns(DeprecationWarning): + enum.IntEnum._convert( + 'UnittestConvert', + ('test.test_enum', '__main__')[__name__=='__main__'], + filter=lambda x: x.startswith('CONVERT_TEST_')) + + @unittest.skipUnless(sys.version_info >= (3, 9), + '_convert was removed in 3.9') + def test_convert_raise(self): + with self.assertRaises(AttributeError): + enum.IntEnum._convert( + 'UnittestConvert', + ('test.test_enum', '__main__')[__name__=='__main__'], + filter=lambda x: x.startswith('CONVERT_TEST_')) + if __name__ == '__main__': unittest.main() diff --git a/Misc/ACKS b/Misc/ACKS index 0d9431d2429..96985358e23 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1178,6 +1178,7 @@ Piet van Oostrum Tomas Oppelstrup Jason Orendorff Bastien Orivel +orlnub123 Douglas Orr William Orr Michele OrrĂ¹ diff --git a/Misc/NEWS.d/next/Library/2018-08-31-06-28-03.bpo-34282.ztyXH8.rst b/Misc/NEWS.d/next/Library/2018-08-31-06-28-03.bpo-34282.ztyXH8.rst new file mode 100644 index 00000000000..79f56f124a3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-08-31-06-28-03.bpo-34282.ztyXH8.rst @@ -0,0 +1,2 @@ +Move ``Enum._convert`` to ``EnumMeta._convert_`` and fix enum members getting +shadowed by parent attributes.