]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-143831: Compare cells by identity in forward references (GH-143848) (#144020)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 19 Jan 2026 05:57:54 +0000 (06:57 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Jan 2026 05:57:54 +0000 (05:57 +0000)
gh-143831: Compare cells by identity in forward references (GH-143848)
(cherry picked from commit 59d3594ca12939dea0a537d9964d8d637546855c)

Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
Lib/annotationlib.py
Lib/test/test_annotationlib.py
Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst [new file with mode: 0644]

index 4085cc6bef7954961d54b1c1b1c2b1277fe68c2a..832d160de7f4e596f7e451f28cd6b8479ba312f3 100644 (file)
@@ -279,7 +279,13 @@ class ForwardRef:
             # because dictionaries are not hashable.
             and self.__globals__ is other.__globals__
             and self.__forward_is_class__ == other.__forward_is_class__
-            and self.__cell__ == other.__cell__
+            # Two separate cells are always considered unequal in forward refs.
+            and (
+                {name: id(cell) for name, cell in self.__cell__.items()}
+                == {name: id(cell) for name, cell in other.__cell__.items()}
+                if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict)
+                else self.__cell__ is other.__cell__
+            )
             and self.__owner__ == other.__owner__
             and (
                 (tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) ==
@@ -293,7 +299,10 @@ class ForwardRef:
             self.__forward_module__,
             id(self.__globals__),  # dictionaries are not hashable, so hash by identity
             self.__forward_is_class__,
-            tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,
+            (  # cells are not hashable as well
+                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,
         ))
index a8537871d294cfba81f7c6d5e9d28cf23a62faf7..6b75da32fa944a8fba882f2c08bf5d3d649286fe 100644 (file)
@@ -8,6 +8,7 @@ import functools
 import itertools
 import pickle
 from string.templatelib import Template, Interpolation
+import types
 import typing
 import sys
 import unittest
@@ -1862,6 +1863,39 @@ class TestForwardRefClass(unittest.TestCase):
         self.assertNotEqual(hash(c3), hash(c4))
         self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__)))
 
+    def test_forward_equality_and_hash_with_cells(self):
+        """Regression test for GH-143831."""
+        class A:
+            def one(_) -> C1:
+                """One cell."""
+
+            one_f = ForwardRef("C1", owner=one)
+            one_f_ga1 = get_annotations(one, format=Format.FORWARDREF)["return"]
+            one_f_ga2 = get_annotations(one, format=Format.FORWARDREF)["return"]
+            self.assertIsInstance(one_f_ga1.__cell__, types.CellType)
+            self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__)
+
+            def two(_) -> C1 | C2:
+                """Two cells."""
+
+            two_f_ga1 = get_annotations(two, format=Format.FORWARDREF)["return"]
+            two_f_ga2 = get_annotations(two, format=Format.FORWARDREF)["return"]
+            self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__)
+            self.assertIsInstance(two_f_ga1.__cell__, dict)
+            self.assertIsInstance(two_f_ga2.__cell__, dict)
+
+        type C1 = None
+        type C2 = None
+
+        self.assertNotEqual(A.one_f, A.one_f_ga1)
+        self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1))
+
+        self.assertEqual(A.one_f_ga1, A.one_f_ga2)
+        self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2))
+
+        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_namespace(self):
         def namespace1():
             a = ForwardRef("A")
diff --git a/Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst b/Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst
new file mode 100644 (file)
index 0000000..620adea
--- /dev/null
@@ -0,0 +1,3 @@
+:class:`annotationlib.ForwardRef` objects are now hashable when created from
+annotation scopes with closures. Previously, hashing such objects would
+throw an exception. Patch by Bartosz SÅ‚awecki.