summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEthan Furman <ethan@stoneleaf.us>2023-04-18 23:19:23 (GMT)
committerGitHub <noreply@github.com>2023-04-18 23:19:23 (GMT)
commit700ec657c80e78fb299963ffaa684c859ddb8f87 (patch)
tree61a25f3c6ed737451201814626b8af53e23db6ba
parent07804ce24c3103ee1bb141af31b9a1a0f92f5e43 (diff)
downloadcpython-700ec657c80e78fb299963ffaa684c859ddb8f87.zip
cpython-700ec657c80e78fb299963ffaa684c859ddb8f87.tar.gz
cpython-700ec657c80e78fb299963ffaa684c859ddb8f87.tar.bz2
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 <3933065+samypr100@users.noreply.github.com>
-rw-r--r--Doc/howto/enum.rst14
-rw-r--r--Doc/library/enum.rst7
-rw-r--r--Lib/enum.py86
-rw-r--r--Lib/test/test_enum.py17
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-17-14-47-28.gh-issue-103596.ME1y3_.rst2
5 files changed, 85 insertions, 41 deletions
diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst
index 56391a0..68b75c5 100644
--- a/Doc/howto/enum.rst
+++ b/Doc/howto/enum.rst
@@ -36,8 +36,10 @@ inherits from :class:`Enum` itself.
.. note:: Case of Enum Members
- Because Enums are used to represent constants we recommend using
- UPPER_CASE names for members, and will be using that style in our examples.
+ Because Enums are used to represent constants, and to help avoid issues
+ with name clashes between mixin-class methods/attributes and enum names,
+ we strongly recommend using UPPER_CASE names for members, and will be using
+ that style in our examples.
Depending on the nature of the enum a member's value may or may not be
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::
Use the :func:`!dataclass` argument ``repr=False``
to use the standard :func:`repr`.
+.. versionchanged:: 3.12
+ Only the dataclass fields are shown in the value area, not the dataclass'
+ name.
+
Pickling
--------
@@ -992,7 +998,9 @@ but remain normal attributes.
Enum members are instances of their enum class, and are normally accessed as
``EnumClass.member``. In certain situations, such as writing custom enum
behavior, being able to access one member directly from another is useful,
-and is supported.
+and is supported; however, in order to avoid name clashes between member names
+and attributes/methods from mixed-in classes, upper-case names are strongly
+recommended.
.. versionchanged:: 3.5
diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index 07acf9d..582e062 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -119,7 +119,8 @@ Module Contents
:func:`~enum.property`
Allows :class:`Enum` members to have attributes without conflicting with
- member names.
+ member names. The ``value`` and ``name`` attributes are implemented this
+ way.
:func:`unique`
@@ -169,7 +170,7 @@ Data Types
final *enum*, as well as creating the enum members, properly handling
duplicates, providing iteration over the enum class, etc.
- .. method:: EnumType.__call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+ .. method:: EnumType.__call__(cls, value, names=None, \*, module=None, qualname=None, type=None, start=1, boundary=None)
This method is called in two different ways:
@@ -317,7 +318,7 @@ Data Types
>>> PowersOfThree.SECOND.value
9
- .. method:: Enum.__init_subclass__(cls, **kwds)
+ .. method:: Enum.__init_subclass__(cls, \**kwds)
A *classmethod* that is used to further configure subsequent subclasses.
By default, does nothing.
diff --git a/Lib/enum.py b/Lib/enum.py
index e9f224a..6e497f7 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -190,6 +190,8 @@ class property(DynamicClassAttribute):
"""
member = None
+ _attr_type = None
+ _cls_type = None
def __get__(self, instance, ownerclass=None):
if instance is None:
@@ -199,33 +201,36 @@ class property(DynamicClassAttribute):
raise AttributeError(
'%r has no attribute %r' % (ownerclass, self.name)
)
- else:
- if self.fget is None:
- # look for a member by this name.
- try:
- return ownerclass._member_map_[self.name]
- except KeyError:
- raise AttributeError(
- '%r has no attribute %r' % (ownerclass, self.name)
- ) from None
- else:
- return self.fget(instance)
+ if self.fget is not None:
+ # use previous enum.property
+ return self.fget(instance)
+ elif self._attr_type == 'attr':
+ # look up previous attibute
+ return getattr(self._cls_type, self.name)
+ elif self._attr_type == 'desc':
+ # use previous descriptor
+ return getattr(instance._value_, self.name)
+ # look for a member by this name.
+ try:
+ return ownerclass._member_map_[self.name]
+ except KeyError:
+ raise AttributeError(
+ '%r has no attribute %r' % (ownerclass, self.name)
+ ) from None
def __set__(self, instance, value):
- if self.fset is None:
- raise AttributeError(
- "<enum %r> cannot set attribute %r" % (self.clsname, self.name)
- )
- else:
+ if self.fset is not None:
return self.fset(instance, value)
+ raise AttributeError(
+ "<enum %r> cannot set attribute %r" % (self.clsname, self.name)
+ )
def __delete__(self, instance):
- if self.fdel is None:
- raise AttributeError(
- "<enum %r> cannot delete attribute %r" % (self.clsname, self.name)
- )
- else:
+ if self.fdel is not None:
return self.fdel(instance)
+ raise AttributeError(
+ "<enum %r> cannot delete attribute %r" % (self.clsname, self.name)
+ )
def __set_name__(self, ownerclass, name):
self.name = name
@@ -313,27 +318,38 @@ class _proto_member:
enum_class._member_names_.append(member_name)
# if necessary, get redirect in place and then add it to _member_map_
found_descriptor = None
+ descriptor_type = None
+ class_type = None
for base in enum_class.__mro__[1:]:
- descriptor = base.__dict__.get(member_name)
- if descriptor is not None:
- if isinstance(descriptor, (property, DynamicClassAttribute)):
- found_descriptor = descriptor
+ attr = base.__dict__.get(member_name)
+ if attr is not None:
+ if isinstance(attr, (property, DynamicClassAttribute)):
+ found_descriptor = attr
+ class_type = base
+ descriptor_type = 'enum'
break
- elif (
- hasattr(descriptor, 'fget') and
- hasattr(descriptor, 'fset') and
- hasattr(descriptor, 'fdel')
- ):
- found_descriptor = descriptor
+ elif _is_descriptor(attr):
+ found_descriptor = attr
+ descriptor_type = descriptor_type or 'desc'
+ class_type = class_type or base
continue
+ else:
+ descriptor_type = 'attr'
+ class_type = base
if found_descriptor:
redirect = property()
redirect.member = enum_member
redirect.__set_name__(enum_class, member_name)
- # earlier descriptor found; copy fget, fset, fdel to this one.
- redirect.fget = found_descriptor.fget
- redirect.fset = found_descriptor.fset
- redirect.fdel = found_descriptor.fdel
+ if descriptor_type in ('enum','desc'):
+ # earlier descriptor found; copy fget, fset, fdel to this one.
+ redirect.fget = getattr(found_descriptor, 'fget', None)
+ redirect._get = getattr(found_descriptor, '__get__', None)
+ redirect.fset = getattr(found_descriptor, 'fset', None)
+ redirect._set = getattr(found_descriptor, '__set__', None)
+ redirect.fdel = getattr(found_descriptor, 'fdel', None)
+ redirect._del = getattr(found_descriptor, '__delete__', None)
+ redirect._attr_type = descriptor_type
+ redirect._cls_type = class_type
setattr(enum_class, member_name, redirect)
else:
setattr(enum_class, member_name, enum_member)
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index e9dfcf8..fb7a016 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -819,10 +819,27 @@ class TestPlainFlag(_EnumTests, _PlainOutputTests, _FlagTests, unittest.TestCase
class TestIntEnum(_EnumTests, _MinimalOutputTests, unittest.TestCase):
enum_type = IntEnum
+ #
+ def test_shadowed_attr(self):
+ class Number(IntEnum):
+ divisor = 1
+ numerator = 2
+ #
+ self.assertEqual(Number.divisor.numerator, 1)
+ self.assertIs(Number.numerator.divisor, Number.divisor)
class TestStrEnum(_EnumTests, _MinimalOutputTests, unittest.TestCase):
enum_type = StrEnum
+ #
+ def test_shadowed_attr(self):
+ class Book(StrEnum):
+ author = 'author'
+ title = 'title'
+ #
+ self.assertEqual(Book.author.title(), 'Author')
+ self.assertEqual(Book.title.title(), 'Title')
+ self.assertIs(Book.title.author, Book.author)
class TestIntFlag(_EnumTests, _MinimalOutputTests, _FlagTests, unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2023-04-17-14-47-28.gh-issue-103596.ME1y3_.rst b/Misc/NEWS.d/next/Library/2023-04-17-14-47-28.gh-issue-103596.ME1y3_.rst
new file mode 100644
index 0000000..2fa27e6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-17-14-47-28.gh-issue-103596.ME1y3_.rst
@@ -0,0 +1,2 @@
+Attributes/methods are no longer shadowed by same-named enum members,
+although they may be shadowed by enum.property's.