Skip to content

Commit 3ecb14d

Browse files
committed
gh-152358: Fix hash() of annotationlib.ForwardRef with unhashable value
1 parent 1812162 commit 3ecb14d

3 files changed

Lines changed: 61 additions & 5 deletions

File tree

Lib/annotationlib.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,15 @@ def __eq__(self, other):
283283
if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict)
284284
else self.__cell__ is other.__cell__
285285
)
286-
and self.__owner__ == other.__owner__
286+
# Owners may be unhashable; __hash__ uses id(), so compare by "is".
287+
and self.__owner__ is other.__owner__
288+
# Compare extra-name values by identity (see __hash__).
287289
and (
288-
(tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) ==
289-
(tuple(sorted(other.__extra_names__.items())) if other.__extra_names__ else None)
290+
{n: id(v) for n, v in self.__extra_names__.items()}
291+
if self.__extra_names__ else None
292+
) == (
293+
{n: id(v) for n, v in other.__extra_names__.items()}
294+
if other.__extra_names__ else None
290295
)
291296
)
292297

@@ -300,8 +305,13 @@ def __hash__(self):
300305
tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()]))
301306
if isinstance(self.__cell__, dict) else id(self.__cell__),
302307
),
303-
self.__owner__,
304-
tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
308+
id(self.__owner__), # owners may be unhashable, so hash by identity
309+
( # extra-name values may be unhashable, so hash by identity
310+
tuple(sorted(
311+
(n, id(v)) for n, v in self.__extra_names__.items()
312+
))
313+
if self.__extra_names__ else None
314+
),
305315
))
306316

307317
def __or__(self, other):

Lib/test/test_annotationlib.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,49 @@ def two(_) -> C1 | C2:
19991999
self.assertEqual(A.two_f_ga1, A.two_f_ga2)
20002000
self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2))
20012001

2002+
def test_forward_equality_and_hash_with_extra_names(self):
2003+
"""Regression test for the __extra_names__ sibling of GH-143831."""
2004+
class Unhashable:
2005+
__hash__ = None
2006+
2007+
# An unhashable value referenced in an annotation is kept in the
2008+
# forward ref's __extra_names__.
2009+
ns = support.run_code(
2010+
"def f(a: undefined | obj): pass",
2011+
extra_names={"obj": Unhashable()},
2012+
)
2013+
fr1 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"]
2014+
fr2 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"]
2015+
self.assertIsInstance(fr1.__extra_names__, dict)
2016+
2017+
self.assertEqual(fr1, fr2)
2018+
self.assertEqual(hash(fr1), hash(fr2))
2019+
self.assertEqual(len({fr1, fr2}), 1)
2020+
2021+
# Forward refs with different extra-name values are unequal.
2022+
ns2 = support.run_code(
2023+
"def g(a: undefined | obj): pass",
2024+
extra_names={"obj": Unhashable()},
2025+
)
2026+
fr3 = get_annotations(ns2["g"], format=Format.FORWARDREF)["a"]
2027+
self.assertNotEqual(fr1, fr3)
2028+
self.assertEqual(len({fr1, fr2, fr3}), 2)
2029+
2030+
def test_forward_equality_and_hash_with_unhashable_owner(self):
2031+
"""Regression test for an unhashable __owner__ (GH-143831 sibling)."""
2032+
class MetaNoHash(type):
2033+
__hash__ = None
2034+
2035+
class D(metaclass=MetaNoHash):
2036+
x: undefined
2037+
2038+
fr1 = get_annotations(D, format=Format.FORWARDREF)["x"]
2039+
fr2 = get_annotations(D, format=Format.FORWARDREF)["x"]
2040+
self.assertIs(fr1.__owner__, D)
2041+
self.assertEqual(fr1, fr2)
2042+
self.assertEqual(hash(fr1), hash(fr2))
2043+
self.assertEqual(len({fr1, fr2}), 1)
2044+
20022045
def test_forward_equality_namespace(self):
20032046
def namespace1():
20042047
a = ForwardRef("A")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`hash` raising :exc:`TypeError` on an
2+
:class:`annotationlib.ForwardRef` that holds an unhashable value.
3+
Patch by tonghuaroot.

0 commit comments

Comments
 (0)