Thanks to visit codestin.com
Credit goes to github.com

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)