| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -945,23 +945,12 @@ but remain normal attributes. | |||
| 945 | 945 | """""""""""""""""""" | |
| 946 | 946 | ||
| 947 | 947 | 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``. | ||
| 962 | 951 | ||
| 963 | 952 | .. versionchanged:: 3.5 | |
| 964 | - .. versionchanged:: 3.11 | ||
| 953 | + .. versionchanged:: 3.12 | ||
| 965 | 954 | ||
| 966 | 955 | ||
| 967 | 956 | Creating members that are mixed with other data types | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -176,13 +176,6 @@ Data Types | |||
| 176 | 176 | >>> dir(Color) | |
| 177 | 177 | ['BLUE', 'GREEN', 'RED', '__class__', '__contains__', '__doc__', '__getitem__', '__init_subclass__', '__iter__', '__len__', '__members__', '__module__', '__name__', '__qualname__'] | |
| 178 | 178 | ||
| 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 | - | ||
| 186 | 179 | .. method:: EnumType.__getitem__(cls, name) | |
| 187 | 180 | ||
| 188 | 181 | Returns the Enum member in *cls* matching *name*, or raises an :exc:`KeyError`:: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -185,19 +185,35 @@ class property(DynamicClassAttribute): | |||
| 185 | 185 | a corresponding enum member. | |
| 186 | 186 | """ | |
| 187 | 187 | ||
| 188 | + member = None | ||
| 189 | + | ||
| 188 | 190 | def __get__(self, instance, ownerclass=None): | |
| 189 | 191 | 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: | ||
| 193 | 195 | raise AttributeError( | |
| 194 | 196 | '%r has no attribute %r' % (ownerclass, self.name) | |
| 195 | 197 | ) | |
| 196 | 198 | else: | |
| 197 | 199 | 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, | ||
| 200 | 211 | ) | |
| 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 | + # ) | ||
| 201 | 217 | else: | |
| 202 | 218 | return self.fget(instance) | |
| 203 | 219 | ||
@@ -299,30 +315,20 @@ def __set_name__(self, enum_class, member_name): | |||
| 299 | 315 | enum_class._member_names_.append(member_name) | |
| 300 | 316 | # get redirect in place before adding to _member_map_ | |
| 301 | 317 | # but check for other instances in parent classes first | |
| 302 | - need_override = False | ||
| 303 | 318 | descriptor = None | |
| 304 | 319 | for base in enum_class.__mro__[1:]: | |
| 305 | 320 | descriptor = base.__dict__.get(member_name) | |
| 306 | 321 | if descriptor is not None: | |
| 307 | 322 | if isinstance(descriptor, (property, DynamicClassAttribute)): | |
| 308 | 323 | 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) | ||
| 326 | 332 | # now add to _member_map_ (even aliases) | |
| 327 | 333 | enum_class._member_map_[member_name] = enum_member | |
| 328 | 334 | try: | |
@@ -740,22 +746,6 @@ def __dir__(cls): | |||
| 740 | 746 | # return whatever mixed-in data type has | |
| 741 | 747 | return sorted(set(dir(cls._member_type_)) | interesting) | |
| 742 | 748 | ||
| 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 | - | ||
| 759 | 749 | def __getitem__(cls, name): | |
| 760 | 750 | """ | |
| 761 | 751 | Return the member matching `name`. | |
@@ -1200,10 +1190,10 @@ def __reduce_ex__(self, proto): | |||
| 1200 | 1190 | # enum.property is used to provide access to the `name` and | |
| 1201 | 1191 | # `value` attributes of enum members while keeping some measure of | |
| 1202 | 1192 | # 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. | ||
| 1207 | 1197 | ||
| 1208 | 1198 | @property | |
| 1209 | 1199 | def name(self): | |
@@ -1677,10 +1667,12 @@ def convert_class(cls): | |||
| 1677 | 1667 | value = gnv(name, 1, len(member_names), gnv_last_values) | |
| 1678 | 1668 | if value in value2member_map: | |
| 1679 | 1669 | # an alias to an existing member | |
| 1670 | + member = value2member_map[value] | ||
| 1680 | 1671 | redirect = property() | |
| 1672 | + redirect.member = member | ||
| 1681 | 1673 | redirect.__set_name__(enum_class, name) | |
| 1682 | 1674 | setattr(enum_class, name, redirect) | |
| 1683 | - member_map[name] = value2member_map[value] | ||
| 1675 | + member_map[name] = member | ||
| 1684 | 1676 | else: | |
| 1685 | 1677 | # create the member | |
| 1686 | 1678 | if use_args: | |
@@ -1696,6 +1688,7 @@ def convert_class(cls): | |||
| 1696 | 1688 | member.__objclass__ = enum_class | |
| 1697 | 1689 | member.__init__(value) | |
| 1698 | 1690 | redirect = property() | |
| 1691 | + redirect.member = member | ||
| 1699 | 1692 | redirect.__set_name__(enum_class, name) | |
| 1700 | 1693 | setattr(enum_class, name, redirect) | |
| 1701 | 1694 | member_map[name] = member | |
@@ -1723,10 +1716,12 @@ def convert_class(cls): | |||
| 1723 | 1716 | value = value.value | |
| 1724 | 1717 | if value in value2member_map: | |
| 1725 | 1718 | # an alias to an existing member | |
| 1719 | + member = value2member_map[value] | ||
| 1726 | 1720 | redirect = property() | |
| 1721 | + redirect.member = member | ||
| 1727 | 1722 | redirect.__set_name__(enum_class, name) | |
| 1728 | 1723 | setattr(enum_class, name, redirect) | |
| 1729 | - member_map[name] = value2member_map[value] | ||
| 1724 | + member_map[name] = member | ||
| 1730 | 1725 | else: | |
| 1731 | 1726 | # create the member | |
| 1732 | 1727 | if use_args: | |
@@ -1743,6 +1738,7 @@ def convert_class(cls): | |||
| 1743 | 1738 | member.__init__(value) | |
| 1744 | 1739 | member._sort_order_ = len(member_names) | |
| 1745 | 1740 | redirect = property() | |
| 1741 | + redirect.member = member | ||
| 1746 | 1742 | redirect.__set_name__(enum_class, name) | |
| 1747 | 1743 | setattr(enum_class, name, redirect) | |
| 1748 | 1744 | member_map[name] = member | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2646,14 +2646,28 @@ class Private(Enum): | |||
| 2646 | 2646 | self.assertEqual(Private._Private__corporal, 'Radar') | |
| 2647 | 2647 | self.assertEqual(Private._Private__major_, 'Hoolihan') | |
| 2648 | 2648 | ||
| 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 | + ) | ||
| 2650 | 2653 | def test_exception_for_member_from_member_access(self): | |
| 2651 | 2654 | with self.assertRaisesRegex(AttributeError, "<enum .Di.> member has no attribute .NO."): | |
| 2652 | 2655 | class Di(Enum): | |
| 2653 | 2656 | YES = 1 | |
| 2654 | 2657 | NO = 0 | |
| 2655 | 2658 | nope = Di.YES.NO | |
| 2656 | 2659 | ||
| 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) | ||
| 2657 | 2671 | ||
| 2658 | 2672 | def test_dynamic_members_with_static_methods(self): | |
| 2659 | 2673 | # | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + Add deprecation warning for enum ``member.member`` access (e.g. ``Color.RED.BLUE``). | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments