Skip to content

Commit 4e704d7

Browse files
authored
gh-95077: [Enum] add code-based deprecation warnings for member.member access (GH-95083)
* issue deprecation warning for member.member access * always store member property in current class * remove __getattr__
1 parent 73ee5a6 commit 4e704d7

File tree

5 files changed

+61
-68
lines changed

5 files changed

+61
-68
lines changed

Doc/howto/enum.rst

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -945,23 +945,12 @@ but remain normal attributes.
945945
""""""""""""""""""""
946946

947947
Enum members are instances of their enum class, and are normally accessed as
948-
``EnumClass.member``. In Python versions ``3.5`` to ``3.10`` you could access
949-
members from other members -- this practice was discouraged, and in ``3.11``
950-
:class:`Enum` returns to not allowing it::
951-
952-
>>> class FieldTypes(Enum):
953-
... name = 0
954-
... value = 1
955-
... size = 2
956-
...
957-
>>> FieldTypes.value.size
958-
Traceback (most recent call last):
959-
...
960-
AttributeError: <enum 'FieldTypes'> member has no attribute 'size'
961-
948+
``EnumClass.member``. In Python versions starting with ``3.5`` you could access
949+
members from other members -- this practice is discouraged, is deprecated
950+
in ``3.12``, and will be removed in ``3.14``.
962951

963952
.. versionchanged:: 3.5
964-
.. versionchanged:: 3.11
953+
.. versionchanged:: 3.12
965954

966955

967956
Creating members that are mixed with other data types

Doc/library/enum.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,6 @@ Data Types
176176
>>> dir(Color)
177177
['BLUE', 'GREEN', 'RED', '__class__', '__contains__', '__doc__', '__getitem__', '__init_subclass__', '__iter__', '__len__', '__members__', '__module__', '__name__', '__qualname__']
178178

179-
.. method:: EnumType.__getattr__(cls, name)
180-
181-
Returns the Enum member in *cls* matching *name*, or raises an :exc:`AttributeError`::
182-
183-
>>> Color.GREEN
184-
<Color.GREEN: 2>
185-
186179
.. method:: EnumType.__getitem__(cls, name)
187180

188181
Returns the Enum member in *cls* matching *name*, or raises an :exc:`KeyError`::

Lib/enum.py

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,35 @@ class property(DynamicClassAttribute):
185185
a corresponding enum member.
186186
"""
187187

188+
member = None
189+
188190
def __get__(self, instance, ownerclass=None):
189191
if instance is None:
190-
try:
191-
return ownerclass._member_map_[self.name]
192-
except KeyError:
192+
if self.member is not None:
193+
return self.member
194+
else:
193195
raise AttributeError(
194196
'%r has no attribute %r' % (ownerclass, self.name)
195197
)
196198
else:
197199
if self.fget is None:
198-
raise AttributeError(
199-
'%r member has no attribute %r' % (ownerclass, self.name)
200+
if self.member is None: # not sure this can happen, but just in case
201+
raise AttributeError(
202+
'%r has no attribute %r' % (ownerclass, self.name)
203+
)
204+
# issue warning deprecating this behavior
205+
import warnings
206+
warnings.warn(
207+
"`member.member` access (e.g. `Color.RED.BLUE`) is "
208+
"deprecated and will be removed in 3.14.",
209+
DeprecationWarning,
210+
stacklevel=2,
200211
)
212+
return self.member
213+
# XXX: uncomment in 3.14 and remove warning above
214+
# raise AttributeError(
215+
# '%r member has no attribute %r' % (ownerclass, self.name)
216+
# )
201217
else:
202218
return self.fget(instance)
203219

@@ -299,30 +315,20 @@ def __set_name__(self, enum_class, member_name):
299315
enum_class._member_names_.append(member_name)
300316
# get redirect in place before adding to _member_map_
301317
# but check for other instances in parent classes first
302-
need_override = False
303318
descriptor = None
304319
for base in enum_class.__mro__[1:]:
305320
descriptor = base.__dict__.get(member_name)
306321
if descriptor is not None:
307322
if isinstance(descriptor, (property, DynamicClassAttribute)):
308323
break
309-
else:
310-
need_override = True
311-
# keep looking for an enum.property
312-
if descriptor and not need_override:
313-
# previous enum.property found, no further action needed
314-
pass
315-
elif descriptor and need_override:
316-
redirect = property()
317-
redirect.__set_name__(enum_class, member_name)
318-
# Previous enum.property found, but some other inherited attribute
319-
# is in the way; copy fget, fset, fdel to this one.
320-
redirect.fget = descriptor.fget
321-
redirect.fset = descriptor.fset
322-
redirect.fdel = descriptor.fdel
323-
setattr(enum_class, member_name, redirect)
324-
else:
325-
setattr(enum_class, member_name, enum_member)
324+
redirect = property()
325+
redirect.member = enum_member
326+
redirect.__set_name__(enum_class, member_name)
327+
if descriptor:
328+
redirect.fget = getattr(descriptor, 'fget', None)
329+
redirect.fset = getattr(descriptor, 'fset', None)
330+
redirect.fdel = getattr(descriptor, 'fdel', None)
331+
setattr(enum_class, member_name, redirect)
326332
# now add to _member_map_ (even aliases)
327333
enum_class._member_map_[member_name] = enum_member
328334
try:
@@ -740,22 +746,6 @@ def __dir__(cls):
740746
# return whatever mixed-in data type has
741747
return sorted(set(dir(cls._member_type_)) | interesting)
742748

743-
def __getattr__(cls, name):
744-
"""
745-
Return the enum member matching `name`
746-
747-
We use __getattr__ instead of descriptors or inserting into the enum
748-
class' __dict__ in order to support `name` and `value` being both
749-
properties for enum members (which live in the class' __dict__) and
750-
enum members themselves.
751-
"""
752-
if _is_dunder(name):
753-
raise AttributeError(name)
754-
try:
755-
return cls._member_map_[name]
756-
except KeyError:
757-
raise AttributeError(name) from None
758-
759749
def __getitem__(cls, name):
760750
"""
761751
Return the member matching `name`.
@@ -1200,10 +1190,10 @@ def __reduce_ex__(self, proto):
12001190
# enum.property is used to provide access to the `name` and
12011191
# `value` attributes of enum members while keeping some measure of
12021192
# protection from modification, while still allowing for an enumeration
1203-
# to have members named `name` and `value`. This works because enumeration
1204-
# members are not set directly on the enum class; they are kept in a
1205-
# separate structure, _member_map_, which is where enum.property looks for
1206-
# them
1193+
# to have members named `name` and `value`. This works because each
1194+
# instance of enum.property saves its companion member, which it returns
1195+
# on class lookup; on instance lookup it either executes a provided function
1196+
# or raises an AttributeError.
12071197

12081198
@property
12091199
def name(self):
@@ -1677,10 +1667,12 @@ def convert_class(cls):
16771667
value = gnv(name, 1, len(member_names), gnv_last_values)
16781668
if value in value2member_map:
16791669
# an alias to an existing member
1670+
member = value2member_map[value]
16801671
redirect = property()
1672+
redirect.member = member
16811673
redirect.__set_name__(enum_class, name)
16821674
setattr(enum_class, name, redirect)
1683-
member_map[name] = value2member_map[value]
1675+
member_map[name] = member
16841676
else:
16851677
# create the member
16861678
if use_args:
@@ -1696,6 +1688,7 @@ def convert_class(cls):
16961688
member.__objclass__ = enum_class
16971689
member.__init__(value)
16981690
redirect = property()
1691+
redirect.member = member
16991692
redirect.__set_name__(enum_class, name)
17001693
setattr(enum_class, name, redirect)
17011694
member_map[name] = member
@@ -1723,10 +1716,12 @@ def convert_class(cls):
17231716
value = value.value
17241717
if value in value2member_map:
17251718
# an alias to an existing member
1719+
member = value2member_map[value]
17261720
redirect = property()
1721+
redirect.member = member
17271722
redirect.__set_name__(enum_class, name)
17281723
setattr(enum_class, name, redirect)
1729-
member_map[name] = value2member_map[value]
1724+
member_map[name] = member
17301725
else:
17311726
# create the member
17321727
if use_args:
@@ -1743,6 +1738,7 @@ def convert_class(cls):
17431738
member.__init__(value)
17441739
member._sort_order_ = len(member_names)
17451740
redirect = property()
1741+
redirect.member = member
17461742
redirect.__set_name__(enum_class, name)
17471743
setattr(enum_class, name, redirect)
17481744
member_map[name] = member

Lib/test/test_enum.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2646,14 +2646,28 @@ class Private(Enum):
26462646
self.assertEqual(Private._Private__corporal, 'Radar')
26472647
self.assertEqual(Private._Private__major_, 'Hoolihan')
26482648

2649-
@unittest.skip("Accessing all values retained for performance reasons, see GH-93910")
2649+
@unittest.skipIf(
2650+
python_version <= (3, 13),
2651+
'member.member access currently deprecated',
2652+
)
26502653
def test_exception_for_member_from_member_access(self):
26512654
with self.assertRaisesRegex(AttributeError, "<enum .Di.> member has no attribute .NO."):
26522655
class Di(Enum):
26532656
YES = 1
26542657
NO = 0
26552658
nope = Di.YES.NO
26562659

2660+
@unittest.skipIf(
2661+
python_version > (3, 13),
2662+
'member.member access now raises',
2663+
)
2664+
def test_warning_for_member_from_member_access(self):
2665+
with self.assertWarnsRegex(DeprecationWarning, '`member.member` access .* is deprecated and will be removed in 3.14'):
2666+
class Di(Enum):
2667+
YES = 1
2668+
NO = 0
2669+
warn = Di.YES.NO
2670+
self.assertIs(warn, Di.NO)
26572671

26582672
def test_dynamic_members_with_static_methods(self):
26592673
#
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add deprecation warning for enum ``member.member`` access (e.g. ``Color.RED.BLUE``).

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy