From 3ecb14d605450b4748cd96b65d391f10bb9ef6b5 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 27 Jun 2026 11:20:03 +0800 Subject: [PATCH] gh-152358: Fix hash() of annotationlib.ForwardRef with unhashable value --- Lib/annotationlib.py | 20 ++++++--- Lib/test/test_annotationlib.py | 43 +++++++++++++++++++ ...-06-27-03-20-03.gh-issue-152358.uhR99k.rst | 3 ++ 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-27-03-20-03.gh-issue-152358.uhR99k.rst diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2b..5d44a4e5067119f 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -283,10 +283,15 @@ def __eq__(self, other): if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict) else self.__cell__ is other.__cell__ ) - and self.__owner__ == other.__owner__ + # Owners may be unhashable; __hash__ uses id(), so compare by "is". + and self.__owner__ is other.__owner__ + # Compare extra-name values by identity (see __hash__). and ( - (tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) == - (tuple(sorted(other.__extra_names__.items())) if other.__extra_names__ else None) + {n: id(v) for n, v in self.__extra_names__.items()} + if self.__extra_names__ else None + ) == ( + {n: id(v) for n, v in other.__extra_names__.items()} + if other.__extra_names__ else None ) ) @@ -300,8 +305,13 @@ def __hash__(self): tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()])) if isinstance(self.__cell__, dict) else id(self.__cell__), ), - self.__owner__, - tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None, + id(self.__owner__), # owners may be unhashable, so hash by identity + ( # extra-name values may be unhashable, so hash by identity + tuple(sorted( + (n, id(v)) for n, v in self.__extra_names__.items() + )) + if self.__extra_names__ else None + ), )) def __or__(self, other): diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 5087c3ca425f1fc..68da6c41d0815f3 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1999,6 +1999,49 @@ def two(_) -> C1 | C2: self.assertEqual(A.two_f_ga1, A.two_f_ga2) self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2)) + def test_forward_equality_and_hash_with_extra_names(self): + """Regression test for the __extra_names__ sibling of GH-143831.""" + class Unhashable: + __hash__ = None + + # An unhashable value referenced in an annotation is kept in the + # forward ref's __extra_names__. + ns = support.run_code( + "def f(a: undefined | obj): pass", + extra_names={"obj": Unhashable()}, + ) + fr1 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"] + fr2 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"] + self.assertIsInstance(fr1.__extra_names__, dict) + + self.assertEqual(fr1, fr2) + self.assertEqual(hash(fr1), hash(fr2)) + self.assertEqual(len({fr1, fr2}), 1) + + # Forward refs with different extra-name values are unequal. + ns2 = support.run_code( + "def g(a: undefined | obj): pass", + extra_names={"obj": Unhashable()}, + ) + fr3 = get_annotations(ns2["g"], format=Format.FORWARDREF)["a"] + self.assertNotEqual(fr1, fr3) + self.assertEqual(len({fr1, fr2, fr3}), 2) + + def test_forward_equality_and_hash_with_unhashable_owner(self): + """Regression test for an unhashable __owner__ (GH-143831 sibling).""" + class MetaNoHash(type): + __hash__ = None + + class D(metaclass=MetaNoHash): + x: undefined + + fr1 = get_annotations(D, format=Format.FORWARDREF)["x"] + fr2 = get_annotations(D, format=Format.FORWARDREF)["x"] + self.assertIs(fr1.__owner__, D) + self.assertEqual(fr1, fr2) + self.assertEqual(hash(fr1), hash(fr2)) + self.assertEqual(len({fr1, fr2}), 1) + def test_forward_equality_namespace(self): def namespace1(): a = ForwardRef("A") diff --git a/Misc/NEWS.d/next/Library/2026-06-27-03-20-03.gh-issue-152358.uhR99k.rst b/Misc/NEWS.d/next/Library/2026-06-27-03-20-03.gh-issue-152358.uhR99k.rst new file mode 100644 index 000000000000000..8a3c5cb3f6d6bd1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-27-03-20-03.gh-issue-152358.uhR99k.rst @@ -0,0 +1,3 @@ +Fix :func:`hash` raising :exc:`TypeError` on an +:class:`annotationlib.ForwardRef` that holds an unhashable value. +Patch by tonghuaroot.