From 5bb0538f6e79acb6d414b7c6c75d3b9a70e9bb6b Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:29:42 +0200 Subject: [PATCH] [3.13] gh-125710: [Enum] fix hashable<->nonhashable comparisons for member values (GH-125735) (GH-125851) gh-125710: [Enum] fix hashable<->nonhashable comparisons for member values (GH-125735) (cherry picked from commit aaed91cabcedc16c089c4b1c9abb1114659a83d3) Co-authored-by: Ethan Furman --- Lib/enum.py | 26 ++++++++++++++----- Lib/test/test_enum.py | 7 +++++ ...-10-19-13-37-37.gh-issue-125710.FyFAAr.rst | 1 + 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-10-19-13-37-37.gh-issue-125710.FyFAAr.rst diff --git a/Lib/enum.py b/Lib/enum.py index 78df81d24a9..ac13e2c7330 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -328,6 +328,8 @@ class _proto_member: # to the map, and by-value lookups for this value will be # linear. enum_class._value2member_map_.setdefault(value, enum_member) + if value not in enum_class._hashable_values_: + enum_class._hashable_values_.append(value) except TypeError: # keep track of the value in a list so containment checks are quick enum_class._unhashable_values_.append(value) @@ -545,7 +547,8 @@ class EnumType(type): classdict['_member_names_'] = [] classdict['_member_map_'] = {} classdict['_value2member_map_'] = {} - classdict['_unhashable_values_'] = [] + classdict['_hashable_values_'] = [] # for comparing with non-hashable types + classdict['_unhashable_values_'] = [] # e.g. frozenset() with set() classdict['_unhashable_values_map_'] = {} classdict['_member_type_'] = member_type # now set the __repr__ for the value @@ -755,7 +758,10 @@ class EnumType(type): try: return value in cls._value2member_map_ except TypeError: - return value in cls._unhashable_values_ + return ( + value in cls._unhashable_values_ # both structures are lists + or value in cls._hashable_values_ + ) def __delattr__(cls, attr): # nicer error message when someone tries to delete an attribute @@ -1165,8 +1171,11 @@ class Enum(metaclass=EnumType): pass except TypeError: # not there, now do long search -- O(n) behavior - for name, values in cls._unhashable_values_map_.items(): - if value in values: + for name, unhashable_values in cls._unhashable_values_map_.items(): + if value in unhashable_values: + return cls[name] + for name, member in cls._member_map_.items(): + if value == member._value_: return cls[name] # still not found -- verify that members exist, in-case somebody got here mistakenly # (such as via super when trying to override __new__) @@ -1232,6 +1241,7 @@ class Enum(metaclass=EnumType): # to the map, and by-value lookups for this value will be # linear. cls._value2member_map_.setdefault(value, self) + cls._hashable_values_.append(value) except TypeError: # keep track of the value in a list so containment checks are quick cls._unhashable_values_.append(value) @@ -1762,6 +1772,7 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None): body['_member_names_'] = member_names = [] body['_member_map_'] = member_map = {} body['_value2member_map_'] = value2member_map = {} + body['_hashable_values_'] = hashable_values = [] body['_unhashable_values_'] = unhashable_values = [] body['_unhashable_values_map_'] = {} body['_member_type_'] = member_type = etype._member_type_ @@ -1825,7 +1836,7 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None): contained = value2member_map.get(member._value_) except TypeError: contained = None - if member._value_ in unhashable_values: + if member._value_ in unhashable_values or member.value in hashable_values: for m in enum_class: if m._value_ == member._value_: contained = m @@ -1845,6 +1856,7 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None): else: enum_class._add_member_(name, member) value2member_map[value] = member + hashable_values.append(value) if _is_single_bit(value): # not a multi-bit alias, record in _member_names_ and _flag_mask_ member_names.append(name) @@ -1881,7 +1893,7 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None): contained = value2member_map.get(member._value_) except TypeError: contained = None - if member._value_ in unhashable_values: + if member._value_ in unhashable_values or member._value_ in hashable_values: for m in enum_class: if m._value_ == member._value_: contained = m @@ -1907,6 +1919,8 @@ def _simple_enum(etype=Enum, *, boundary=None, use_args=None): # to the map, and by-value lookups for this value will be # linear. enum_class._value2member_map_.setdefault(value, member) + if value not in hashable_values: + hashable_values.append(value) except TypeError: # keep track of the value in a list so containment checks are quick enum_class._unhashable_values_.append(value) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 46f57b2d9b6..a99347191f8 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3474,6 +3474,13 @@ class TestSpecial(unittest.TestCase): self.assertRaisesRegex(TypeError, '.int. object is not iterable', Enum, 'bad_enum', names=0) self.assertRaisesRegex(TypeError, '.int. object is not iterable', Enum, 'bad_enum', 0, type=int) + def test_nonhashable_matches_hashable(self): # issue 125710 + class Directions(Enum): + DOWN_ONLY = frozenset({"sc"}) + UP_ONLY = frozenset({"cs"}) + UNRESTRICTED = frozenset({"sc", "cs"}) + self.assertIs(Directions({"sc"}), Directions.DOWN_ONLY) + class TestOrder(unittest.TestCase): "test usage of the `_order_` attribute" diff --git a/Misc/NEWS.d/next/Library/2024-10-19-13-37-37.gh-issue-125710.FyFAAr.rst b/Misc/NEWS.d/next/Library/2024-10-19-13-37-37.gh-issue-125710.FyFAAr.rst new file mode 100644 index 00000000000..8d5220e9889 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-19-13-37-37.gh-issue-125710.FyFAAr.rst @@ -0,0 +1 @@ +[Enum] fix hashable<->nonhashable comparisons for member values