Skip to content

Commit 0fb9fad

Browse files
orlnub123ethanfurman
authored andcommitted
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
1 parent f522374 commit 0fb9fad

File tree

7 files changed

+99
-53
lines changed

7 files changed

+99
-53
lines changed

Lib/enum.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,11 @@ def __new__(metacls, cls, bases, classdict):
165165
enum_class._member_map_ = {} # name->value map
166166
enum_class._member_type_ = member_type
167167

168-
# save attributes from super classes so we know if we can take
169-
# the shortcut of storing members in the class dict
170-
base_attributes = {a for b in enum_class.mro() for a in b.__dict__}
168+
# save DynamicClassAttribute attributes from super classes so we know
169+
# if we can take the shortcut of storing members in the class dict
170+
dynamic_attributes = {k for c in enum_class.mro()
171+
for k, v in c.__dict__.items()
172+
if isinstance(v, DynamicClassAttribute)}
171173

172174
# Reverse value->name map for hashable values.
173175
enum_class._value2member_map_ = {}
@@ -227,7 +229,7 @@ def __new__(metacls, cls, bases, classdict):
227229
enum_class._member_names_.append(member_name)
228230
# performance boost for any member that would not shadow
229231
# a DynamicClassAttribute
230-
if member_name not in base_attributes:
232+
if member_name not in dynamic_attributes:
231233
setattr(enum_class, member_name, enum_member)
232234
# now add to _member_map_
233235
enum_class._member_map_[member_name] = enum_member
@@ -428,6 +430,45 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s
428430

429431
return enum_class
430432

433+
def _convert_(cls, name, module, filter, source=None):
434+
"""
435+
Create a new Enum subclass that replaces a collection of global constants
436+
"""
437+
# convert all constants from source (or module) that pass filter() to
438+
# a new Enum called name, and export the enum and its members back to
439+
# module;
440+
# also, replace the __reduce_ex__ method so unpickling works in
441+
# previous Python versions
442+
module_globals = vars(sys.modules[module])
443+
if source:
444+
source = vars(source)
445+
else:
446+
source = module_globals
447+
# _value2member_map_ is populated in the same order every time
448+
# for a consistent reverse mapping of number to name when there
449+
# are multiple names for the same number.
450+
members = [
451+
(name, value)
452+
for name, value in source.items()
453+
if filter(name)]
454+
try:
455+
# sort by value
456+
members.sort(key=lambda t: (t[1], t[0]))
457+
except TypeError:
458+
# unless some values aren't comparable, in which case sort by name
459+
members.sort(key=lambda t: t[0])
460+
cls = cls(name, members, module=module)
461+
cls.__reduce_ex__ = _reduce_ex_by_name
462+
module_globals.update(cls.__members__)
463+
module_globals[name] = cls
464+
return cls
465+
466+
def _convert(cls, *args, **kwargs):
467+
import warnings
468+
warnings.warn("_convert is deprecated and will be removed in 3.9, use "
469+
"_convert_ instead.", DeprecationWarning, stacklevel=2)
470+
return cls._convert_(*args, **kwargs)
471+
431472
@staticmethod
432473
def _get_mixins_(bases):
433474
"""Returns the type for creating enum members, and the first inherited
@@ -613,40 +654,6 @@ def value(self):
613654
"""The value of the Enum member."""
614655
return self._value_
615656

616-
@classmethod
617-
def _convert(cls, name, module, filter, source=None):
618-
"""
619-
Create a new Enum subclass that replaces a collection of global constants
620-
"""
621-
# convert all constants from source (or module) that pass filter() to
622-
# a new Enum called name, and export the enum and its members back to
623-
# module;
624-
# also, replace the __reduce_ex__ method so unpickling works in
625-
# previous Python versions
626-
module_globals = vars(sys.modules[module])
627-
if source:
628-
source = vars(source)
629-
else:
630-
source = module_globals
631-
# _value2member_map_ is populated in the same order every time
632-
# for a consistent reverse mapping of number to name when there
633-
# are multiple names for the same number.
634-
members = [
635-
(name, value)
636-
for name, value in source.items()
637-
if filter(name)]
638-
try:
639-
# sort by value
640-
members.sort(key=lambda t: (t[1], t[0]))
641-
except TypeError:
642-
# unless some values aren't comparable, in which case sort by name
643-
members.sort(key=lambda t: t[0])
644-
cls = cls(name, members, module=module)
645-
cls.__reduce_ex__ = _reduce_ex_by_name
646-
module_globals.update(cls.__members__)
647-
module_globals[name] = cls
648-
return cls
649-
650657

651658
class IntEnum(int, Enum):
652659
"""Enum where members are also (and must be) ints"""

Lib/signal.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55

66
_globals = globals()
77

8-
_IntEnum._convert(
8+
_IntEnum._convert_(
99
'Signals', __name__,
1010
lambda name:
1111
name.isupper()
1212
and (name.startswith('SIG') and not name.startswith('SIG_'))
1313
or name.startswith('CTRL_'))
1414

15-
_IntEnum._convert(
15+
_IntEnum._convert_(
1616
'Handlers', __name__,
1717
lambda name: name in ('SIG_DFL', 'SIG_IGN'))
1818

1919
if 'pthread_sigmask' in _globals:
20-
_IntEnum._convert(
20+
_IntEnum._convert_(
2121
'Sigmasks', __name__,
2222
lambda name: name in ('SIG_BLOCK', 'SIG_UNBLOCK', 'SIG_SETMASK'))
2323

Lib/socket.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,22 @@
7070
# in this module understands the enums and translates them back from integers
7171
# where needed (e.g. .family property of a socket object).
7272

73-
IntEnum._convert(
73+
IntEnum._convert_(
7474
'AddressFamily',
7575
__name__,
7676
lambda C: C.isupper() and C.startswith('AF_'))
7777

78-
IntEnum._convert(
78+
IntEnum._convert_(
7979
'SocketKind',
8080
__name__,
8181
lambda C: C.isupper() and C.startswith('SOCK_'))
8282

83-
IntFlag._convert(
83+
IntFlag._convert_(
8484
'MsgFlag',
8585
__name__,
8686
lambda C: C.isupper() and C.startswith('MSG_'))
8787

88-
IntFlag._convert(
88+
IntFlag._convert_(
8989
'AddressInfo',
9090
__name__,
9191
lambda C: C.isupper() and C.startswith('AI_'))

Lib/ssl.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,32 +119,32 @@
119119
from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION
120120

121121

122-
_IntEnum._convert(
122+
_IntEnum._convert_(
123123
'_SSLMethod', __name__,
124124
lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23',
125125
source=_ssl)
126126

127-
_IntFlag._convert(
127+
_IntFlag._convert_(
128128
'Options', __name__,
129129
lambda name: name.startswith('OP_'),
130130
source=_ssl)
131131

132-
_IntEnum._convert(
132+
_IntEnum._convert_(
133133
'AlertDescription', __name__,
134134
lambda name: name.startswith('ALERT_DESCRIPTION_'),
135135
source=_ssl)
136136

137-
_IntEnum._convert(
137+
_IntEnum._convert_(
138138
'SSLErrorNumber', __name__,
139139
lambda name: name.startswith('SSL_ERROR_'),
140140
source=_ssl)
141141

142-
_IntFlag._convert(
142+
_IntFlag._convert_(
143143
'VerifyFlags', __name__,
144144
lambda name: name.startswith('VERIFY_'),
145145
source=_ssl)
146146

147-
_IntEnum._convert(
147+
_IntEnum._convert_(
148148
'VerifyMode', __name__,
149149
lambda name: name.startswith('CERT_'),
150150
source=_ssl)

Lib/test/test_enum.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enum
22
import inspect
33
import pydoc
4+
import sys
45
import unittest
56
import threading
67
from collections import OrderedDict
@@ -1511,6 +1512,23 @@ class MoreColor(Color):
15111512
yellow = 6
15121513
self.assertEqual(MoreColor.magenta.hex(), '5 hexlified!')
15131514

1515+
def test_subclass_duplicate_name(self):
1516+
class Base(Enum):
1517+
def test(self):
1518+
pass
1519+
class Test(Base):
1520+
test = 1
1521+
self.assertIs(type(Test.test), Test)
1522+
1523+
def test_subclass_duplicate_name_dynamic(self):
1524+
from types import DynamicClassAttribute
1525+
class Base(Enum):
1526+
@DynamicClassAttribute
1527+
def test(self):
1528+
return 'dynamic'
1529+
class Test(Base):
1530+
test = 1
1531+
self.assertEqual(Test.test.test, 'dynamic')
15141532

15151533
def test_no_duplicates(self):
15161534
class UniqueEnum(Enum):
@@ -2668,7 +2686,7 @@ def test__all__(self):
26682686

26692687
class TestIntEnumConvert(unittest.TestCase):
26702688
def test_convert_value_lookup_priority(self):
2671-
test_type = enum.IntEnum._convert(
2689+
test_type = enum.IntEnum._convert_(
26722690
'UnittestConvert',
26732691
('test.test_enum', '__main__')[__name__=='__main__'],
26742692
filter=lambda x: x.startswith('CONVERT_TEST_'))
@@ -2678,7 +2696,7 @@ def test_convert_value_lookup_priority(self):
26782696
self.assertEqual(test_type(5).name, 'CONVERT_TEST_NAME_A')
26792697

26802698
def test_convert(self):
2681-
test_type = enum.IntEnum._convert(
2699+
test_type = enum.IntEnum._convert_(
26822700
'UnittestConvert',
26832701
('test.test_enum', '__main__')[__name__=='__main__'],
26842702
filter=lambda x: x.startswith('CONVERT_TEST_'))
@@ -2694,6 +2712,24 @@ def test_convert(self):
26942712
if name[0:2] not in ('CO', '__')],
26952713
[], msg='Names other than CONVERT_TEST_* found.')
26962714

2715+
@unittest.skipUnless(sys.version_info[:2] == (3, 8),
2716+
'_convert was deprecated in 3.8')
2717+
def test_convert_warn(self):
2718+
with self.assertWarns(DeprecationWarning):
2719+
enum.IntEnum._convert(
2720+
'UnittestConvert',
2721+
('test.test_enum', '__main__')[__name__=='__main__'],
2722+
filter=lambda x: x.startswith('CONVERT_TEST_'))
2723+
2724+
@unittest.skipUnless(sys.version_info >= (3, 9),
2725+
'_convert was removed in 3.9')
2726+
def test_convert_raise(self):
2727+
with self.assertRaises(AttributeError):
2728+
enum.IntEnum._convert(
2729+
'UnittestConvert',
2730+
('test.test_enum', '__main__')[__name__=='__main__'],
2731+
filter=lambda x: x.startswith('CONVERT_TEST_'))
2732+
26972733

26982734
if __name__ == '__main__':
26992735
unittest.main()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,7 @@ Piet van Oostrum
11781178
Tomas Oppelstrup
11791179
Jason Orendorff
11801180
Bastien Orivel
1181+
orlnub123
11811182
Douglas Orr
11821183
William Orr
11831184
Michele Orrù
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Move ``Enum._convert`` to ``EnumMeta._convert_`` and fix enum members getting
2+
shadowed by parent attributes.

0 commit comments

Comments
 (0)