]> granicus.if.org Git - python/commitdiff
bpo-34282: Fix Enum._convert shadowing members named _convert (GH-8568)
authororlnub123 <orlnub123@gmail.com>
Wed, 12 Sep 2018 17:28:53 +0000 (20:28 +0300)
committerEthan Furman <ethan@stoneleaf.us>
Wed, 12 Sep 2018 17:28:53 +0000 (10:28 -0700)
* Fix enum members getting shadowed by parent attributes
* Move Enum._convert to EnumMeta._convert_
* Deprecate _convert

Lib/enum.py
Lib/signal.py
Lib/socket.py
Lib/ssl.py
Lib/test/test_enum.py
Misc/ACKS
Misc/NEWS.d/next/Library/2018-08-31-06-28-03.bpo-34282.ztyXH8.rst [new file with mode: 0644]

index 9d1aef372c12fc247b5dd82bfd6b8aca7270c901..0839671cca00055162749df6fbe1ebc9a211b78e 100644 (file)
@@ -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"""
index 826b62cf596ccf186a53d0c172de237497f2772e..d4a6d6fe2ada8b096a11c9f069efbc7e0ac9a6fd 100644 (file)
@@ -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'))
 
index cfa605a22ada921e1a40148010d273a47a3f3d86..385844b58532021f8042b6f2d7c23e9757344a9f 100644 (file)
@@ -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_'))
index fdd1615744347ec2f57dde0107c0653ad702249f..fa7c152ade911f67e4fd52ee5eb9bebdf2d8780b 100644 (file)
@@ -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)
index 68483e6542549685534c546e7039365aec11994b..c04d03f37523d8fb162c0f591221f3d29f49f614 100644 (file)
@@ -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()
index 0d9431d24290e6c026e22d8ba34fafd5c5bfe190..96985358e2363f840dddec1b87ff89df38fc760c 100644 (file)
--- 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 (file)
index 0000000..79f56f1
--- /dev/null
@@ -0,0 +1,2 @@
+Move ``Enum._convert`` to ``EnumMeta._convert_`` and fix enum members getting
+shadowed by parent attributes.