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
This commit is contained in:
orlnub123 2018-09-12 20:28:53 +03:00 committed by Ethan Furman
parent f52237400b
commit 0fb9fadd3b
7 changed files with 99 additions and 53 deletions

View File

@ -165,9 +165,11 @@ class EnumMeta(type):
enum_class._member_map_ = {} # name->value map enum_class._member_map_ = {} # name->value map
enum_class._member_type_ = member_type enum_class._member_type_ = member_type
# save attributes from super classes so we know if we can take # save DynamicClassAttribute attributes from super classes so we know
# the shortcut of storing members in the class dict # 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__} 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. # Reverse value->name map for hashable values.
enum_class._value2member_map_ = {} enum_class._value2member_map_ = {}
@ -227,7 +229,7 @@ class EnumMeta(type):
enum_class._member_names_.append(member_name) enum_class._member_names_.append(member_name)
# performance boost for any member that would not shadow # performance boost for any member that would not shadow
# a DynamicClassAttribute # a DynamicClassAttribute
if member_name not in base_attributes: if member_name not in dynamic_attributes:
setattr(enum_class, member_name, enum_member) setattr(enum_class, member_name, enum_member)
# now add to _member_map_ # now add to _member_map_
enum_class._member_map_[member_name] = enum_member enum_class._member_map_[member_name] = enum_member
@ -428,6 +430,45 @@ class EnumMeta(type):
return enum_class 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 @staticmethod
def _get_mixins_(bases): def _get_mixins_(bases):
"""Returns the type for creating enum members, and the first inherited """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.""" """The value of the Enum member."""
return self._value_ 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): class IntEnum(int, Enum):
"""Enum where members are also (and must be) ints""" """Enum where members are also (and must be) ints"""

View File

@ -5,19 +5,19 @@ from enum import IntEnum as _IntEnum
_globals = globals() _globals = globals()
_IntEnum._convert( _IntEnum._convert_(
'Signals', __name__, 'Signals', __name__,
lambda name: lambda name:
name.isupper() name.isupper()
and (name.startswith('SIG') and not name.startswith('SIG_')) and (name.startswith('SIG') and not name.startswith('SIG_'))
or name.startswith('CTRL_')) or name.startswith('CTRL_'))
_IntEnum._convert( _IntEnum._convert_(
'Handlers', __name__, 'Handlers', __name__,
lambda name: name in ('SIG_DFL', 'SIG_IGN')) lambda name: name in ('SIG_DFL', 'SIG_IGN'))
if 'pthread_sigmask' in _globals: if 'pthread_sigmask' in _globals:
_IntEnum._convert( _IntEnum._convert_(
'Sigmasks', __name__, 'Sigmasks', __name__,
lambda name: name in ('SIG_BLOCK', 'SIG_UNBLOCK', 'SIG_SETMASK')) lambda name: name in ('SIG_BLOCK', 'SIG_UNBLOCK', 'SIG_SETMASK'))

View File

@ -70,22 +70,22 @@ __all__.extend(os._get_exports_list(_socket))
# in this module understands the enums and translates them back from integers # in this module understands the enums and translates them back from integers
# where needed (e.g. .family property of a socket object). # where needed (e.g. .family property of a socket object).
IntEnum._convert( IntEnum._convert_(
'AddressFamily', 'AddressFamily',
__name__, __name__,
lambda C: C.isupper() and C.startswith('AF_')) lambda C: C.isupper() and C.startswith('AF_'))
IntEnum._convert( IntEnum._convert_(
'SocketKind', 'SocketKind',
__name__, __name__,
lambda C: C.isupper() and C.startswith('SOCK_')) lambda C: C.isupper() and C.startswith('SOCK_'))
IntFlag._convert( IntFlag._convert_(
'MsgFlag', 'MsgFlag',
__name__, __name__,
lambda C: C.isupper() and C.startswith('MSG_')) lambda C: C.isupper() and C.startswith('MSG_'))
IntFlag._convert( IntFlag._convert_(
'AddressInfo', 'AddressInfo',
__name__, __name__,
lambda C: C.isupper() and C.startswith('AI_')) lambda C: C.isupper() and C.startswith('AI_'))

View File

@ -119,32 +119,32 @@ from _ssl import (
from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION
_IntEnum._convert( _IntEnum._convert_(
'_SSLMethod', __name__, '_SSLMethod', __name__,
lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23', lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23',
source=_ssl) source=_ssl)
_IntFlag._convert( _IntFlag._convert_(
'Options', __name__, 'Options', __name__,
lambda name: name.startswith('OP_'), lambda name: name.startswith('OP_'),
source=_ssl) source=_ssl)
_IntEnum._convert( _IntEnum._convert_(
'AlertDescription', __name__, 'AlertDescription', __name__,
lambda name: name.startswith('ALERT_DESCRIPTION_'), lambda name: name.startswith('ALERT_DESCRIPTION_'),
source=_ssl) source=_ssl)
_IntEnum._convert( _IntEnum._convert_(
'SSLErrorNumber', __name__, 'SSLErrorNumber', __name__,
lambda name: name.startswith('SSL_ERROR_'), lambda name: name.startswith('SSL_ERROR_'),
source=_ssl) source=_ssl)
_IntFlag._convert( _IntFlag._convert_(
'VerifyFlags', __name__, 'VerifyFlags', __name__,
lambda name: name.startswith('VERIFY_'), lambda name: name.startswith('VERIFY_'),
source=_ssl) source=_ssl)
_IntEnum._convert( _IntEnum._convert_(
'VerifyMode', __name__, 'VerifyMode', __name__,
lambda name: name.startswith('CERT_'), lambda name: name.startswith('CERT_'),
source=_ssl) source=_ssl)

View File

@ -1,6 +1,7 @@
import enum import enum
import inspect import inspect
import pydoc import pydoc
import sys
import unittest import unittest
import threading import threading
from collections import OrderedDict from collections import OrderedDict
@ -1511,6 +1512,23 @@ class TestEnum(unittest.TestCase):
yellow = 6 yellow = 6
self.assertEqual(MoreColor.magenta.hex(), '5 hexlified!') 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): def test_no_duplicates(self):
class UniqueEnum(Enum): class UniqueEnum(Enum):
@ -2668,7 +2686,7 @@ CONVERT_TEST_NAME_F = 5
class TestIntEnumConvert(unittest.TestCase): class TestIntEnumConvert(unittest.TestCase):
def test_convert_value_lookup_priority(self): def test_convert_value_lookup_priority(self):
test_type = enum.IntEnum._convert( test_type = enum.IntEnum._convert_(
'UnittestConvert', 'UnittestConvert',
('test.test_enum', '__main__')[__name__=='__main__'], ('test.test_enum', '__main__')[__name__=='__main__'],
filter=lambda x: x.startswith('CONVERT_TEST_')) 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') self.assertEqual(test_type(5).name, 'CONVERT_TEST_NAME_A')
def test_convert(self): def test_convert(self):
test_type = enum.IntEnum._convert( test_type = enum.IntEnum._convert_(
'UnittestConvert', 'UnittestConvert',
('test.test_enum', '__main__')[__name__=='__main__'], ('test.test_enum', '__main__')[__name__=='__main__'],
filter=lambda x: x.startswith('CONVERT_TEST_')) filter=lambda x: x.startswith('CONVERT_TEST_'))
@ -2694,6 +2712,24 @@ class TestIntEnumConvert(unittest.TestCase):
if name[0:2] not in ('CO', '__')], if name[0:2] not in ('CO', '__')],
[], msg='Names other than CONVERT_TEST_* found.') [], 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1178,6 +1178,7 @@ Piet van Oostrum
Tomas Oppelstrup Tomas Oppelstrup
Jason Orendorff Jason Orendorff
Bastien Orivel Bastien Orivel
orlnub123
Douglas Orr Douglas Orr
William Orr William Orr
Michele Orrù Michele Orrù

View File

@ -0,0 +1,2 @@
Move ``Enum._convert`` to ``EnumMeta._convert_`` and fix enum members getting
shadowed by parent attributes.