Skip to content

Commit 700ec65

Browse files
gh-103596: [Enum] do not shadow mixed-in methods/attributes (GH-103600)
For example: class Book(StrEnum): title = auto() author = auto() desc = auto() Book.author.desc is Book.desc but Book.author.title() == 'Author' is commonly expected. Using upper-case member names avoids this confusion and possible performance impacts. Co-authored-by: samypr100 <[email protected]>
1 parent 07804ce commit 700ec65

File tree

5 files changed

+85
-41
lines changed

5 files changed

+85
-41
lines changed

Doc/howto/enum.rst

+11-3
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ inherits from :class:`Enum` itself.
3636

3737
.. note:: Case of Enum Members
3838

39-
Because Enums are used to represent constants we recommend using
40-
UPPER_CASE names for members, and will be using that style in our examples.
39+
Because Enums are used to represent constants, and to help avoid issues
40+
with name clashes between mixin-class methods/attributes and enum names,
41+
we strongly recommend using UPPER_CASE names for members, and will be using
42+
that style in our examples.
4143

4244
Depending on the nature of the enum a member's value may or may not be
4345
important, but either way that value can be used to get the corresponding
@@ -490,6 +492,10 @@ the :meth:`~Enum.__repr__` omits the inherited class' name. For example::
490492
Use the :func:`!dataclass` argument ``repr=False``
491493
to use the standard :func:`repr`.
492494

495+
.. versionchanged:: 3.12
496+
Only the dataclass fields are shown in the value area, not the dataclass'
497+
name.
498+
493499

494500
Pickling
495501
--------
@@ -992,7 +998,9 @@ but remain normal attributes.
992998
Enum members are instances of their enum class, and are normally accessed as
993999
``EnumClass.member``. In certain situations, such as writing custom enum
9941000
behavior, being able to access one member directly from another is useful,
995-
and is supported.
1001+
and is supported; however, in order to avoid name clashes between member names
1002+
and attributes/methods from mixed-in classes, upper-case names are strongly
1003+
recommended.
9961004

9971005
.. versionchanged:: 3.5
9981006

Doc/library/enum.rst

+4-3
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ Module Contents
119119
:func:`~enum.property`
120120

121121
Allows :class:`Enum` members to have attributes without conflicting with
122-
member names.
122+
member names. The ``value`` and ``name`` attributes are implemented this
123+
way.
123124

124125
:func:`unique`
125126

@@ -169,7 +170,7 @@ Data Types
169170
final *enum*, as well as creating the enum members, properly handling
170171
duplicates, providing iteration over the enum class, etc.
171172

172-
.. method:: EnumType.__call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
173+
.. method:: EnumType.__call__(cls, value, names=None, \*, module=None, qualname=None, type=None, start=1, boundary=None)
173174

174175
This method is called in two different ways:
175176

@@ -317,7 +318,7 @@ Data Types
317318
>>> PowersOfThree.SECOND.value
318319
9
319320

320-
.. method:: Enum.__init_subclass__(cls, **kwds)
321+
.. method:: Enum.__init_subclass__(cls, \**kwds)
321322

322323
A *classmethod* that is used to further configure subsequent subclasses.
323324
By default, does nothing.

Lib/enum.py

+51-35
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ class property(DynamicClassAttribute):
190190
"""
191191

192192
member = None
193+
_attr_type = None
194+
_cls_type = None
193195

194196
def __get__(self, instance, ownerclass=None):
195197
if instance is None:
@@ -199,33 +201,36 @@ def __get__(self, instance, ownerclass=None):
199201
raise AttributeError(
200202
'%r has no attribute %r' % (ownerclass, self.name)
201203
)
202-
else:
203-
if self.fget is None:
204-
# look for a member by this name.
205-
try:
206-
return ownerclass._member_map_[self.name]
207-
except KeyError:
208-
raise AttributeError(
209-
'%r has no attribute %r' % (ownerclass, self.name)
210-
) from None
211-
else:
212-
return self.fget(instance)
204+
if self.fget is not None:
205+
# use previous enum.property
206+
return self.fget(instance)
207+
elif self._attr_type == 'attr':
208+
# look up previous attibute
209+
return getattr(self._cls_type, self.name)
210+
elif self._attr_type == 'desc':
211+
# use previous descriptor
212+
return getattr(instance._value_, self.name)
213+
# look for a member by this name.
214+
try:
215+
return ownerclass._member_map_[self.name]
216+
except KeyError:
217+
raise AttributeError(
218+
'%r has no attribute %r' % (ownerclass, self.name)
219+
) from None
213220

214221
def __set__(self, instance, value):
215-
if self.fset is None:
216-
raise AttributeError(
217-
"<enum %r> cannot set attribute %r" % (self.clsname, self.name)
218-
)
219-
else:
222+
if self.fset is not None:
220223
return self.fset(instance, value)
224+
raise AttributeError(
225+
"<enum %r> cannot set attribute %r" % (self.clsname, self.name)
226+
)
221227

222228
def __delete__(self, instance):
223-
if self.fdel is None:
224-
raise AttributeError(
225-
"<enum %r> cannot delete attribute %r" % (self.clsname, self.name)
226-
)
227-
else:
229+
if self.fdel is not None:
228230
return self.fdel(instance)
231+
raise AttributeError(
232+
"<enum %r> cannot delete attribute %r" % (self.clsname, self.name)
233+
)
229234

230235
def __set_name__(self, ownerclass, name):
231236
self.name = name
@@ -313,27 +318,38 @@ def __set_name__(self, enum_class, member_name):
313318
enum_class._member_names_.append(member_name)
314319
# if necessary, get redirect in place and then add it to _member_map_
315320
found_descriptor = None
321+
descriptor_type = None
322+
class_type = None
316323
for base in enum_class.__mro__[1:]:
317-
descriptor = base.__dict__.get(member_name)
318-
if descriptor is not None:
319-
if isinstance(descriptor, (property, DynamicClassAttribute)):
320-
found_descriptor = descriptor
324+
attr = base.__dict__.get(member_name)
325+
if attr is not None:
326+
if isinstance(attr, (property, DynamicClassAttribute)):
327+
found_descriptor = attr
328+
class_type = base
329+
descriptor_type = 'enum'
321330
break
322-
elif (
323-
hasattr(descriptor, 'fget') and
324-
hasattr(descriptor, 'fset') and
325-
hasattr(descriptor, 'fdel')
326-
):
327-
found_descriptor = descriptor
331+
elif _is_descriptor(attr):
332+
found_descriptor = attr
333+
descriptor_type = descriptor_type or 'desc'
334+
class_type = class_type or base
328335
continue
336+
else:
337+
descriptor_type = 'attr'
338+
class_type = base
329339
if found_descriptor:
330340
redirect = property()
331341
redirect.member = enum_member
332342
redirect.__set_name__(enum_class, member_name)
333-
# earlier descriptor found; copy fget, fset, fdel to this one.
334-
redirect.fget = found_descriptor.fget
335-
redirect.fset = found_descriptor.fset
336-
redirect.fdel = found_descriptor.fdel
343+
if descriptor_type in ('enum','desc'):
344+
# earlier descriptor found; copy fget, fset, fdel to this one.
345+
redirect.fget = getattr(found_descriptor, 'fget', None)
346+
redirect._get = getattr(found_descriptor, '__get__', None)
347+
redirect.fset = getattr(found_descriptor, 'fset', None)
348+
redirect._set = getattr(found_descriptor, '__set__', None)
349+
redirect.fdel = getattr(found_descriptor, 'fdel', None)
350+
redirect._del = getattr(found_descriptor, '__delete__', None)
351+
redirect._attr_type = descriptor_type
352+
redirect._cls_type = class_type
337353
setattr(enum_class, member_name, redirect)
338354
else:
339355
setattr(enum_class, member_name, enum_member)

Lib/test/test_enum.py

+17
Original file line numberDiff line numberDiff line change
@@ -819,10 +819,27 @@ class TestPlainFlag(_EnumTests, _PlainOutputTests, _FlagTests, unittest.TestCase
819819

820820
class TestIntEnum(_EnumTests, _MinimalOutputTests, unittest.TestCase):
821821
enum_type = IntEnum
822+
#
823+
def test_shadowed_attr(self):
824+
class Number(IntEnum):
825+
divisor = 1
826+
numerator = 2
827+
#
828+
self.assertEqual(Number.divisor.numerator, 1)
829+
self.assertIs(Number.numerator.divisor, Number.divisor)
822830

823831

824832
class TestStrEnum(_EnumTests, _MinimalOutputTests, unittest.TestCase):
825833
enum_type = StrEnum
834+
#
835+
def test_shadowed_attr(self):
836+
class Book(StrEnum):
837+
author = 'author'
838+
title = 'title'
839+
#
840+
self.assertEqual(Book.author.title(), 'Author')
841+
self.assertEqual(Book.title.title(), 'Title')
842+
self.assertIs(Book.title.author, Book.author)
826843

827844

828845
class TestIntFlag(_EnumTests, _MinimalOutputTests, _FlagTests, unittest.TestCase):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Attributes/methods are no longer shadowed by same-named enum members,
2+
although they may be shadowed by enum.property's.

0 commit comments

Comments
 (0)