Skip to content

Commit 59d3594

Browse files
authored
gh-143831: Compare cells by identity in forward references (#143848)
1 parent 78b1370 commit 59d3594

File tree

3 files changed

+48
-2
lines changed

3 files changed

+48
-2
lines changed

Lib/annotationlib.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,13 @@ def __eq__(self, other):
279279
# because dictionaries are not hashable.
280280
and self.__globals__ is other.__globals__
281281
and self.__forward_is_class__ == other.__forward_is_class__
282-
and self.__cell__ == other.__cell__
282+
# Two separate cells are always considered unequal in forward refs.
283+
and (
284+
{name: id(cell) for name, cell in self.__cell__.items()}
285+
== {name: id(cell) for name, cell in other.__cell__.items()}
286+
if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict)
287+
else self.__cell__ is other.__cell__
288+
)
283289
and self.__owner__ == other.__owner__
284290
and (
285291
(tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) ==
@@ -293,7 +299,10 @@ def __hash__(self):
293299
self.__forward_module__,
294300
id(self.__globals__), # dictionaries are not hashable, so hash by identity
295301
self.__forward_is_class__,
296-
tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,
302+
( # cells are not hashable as well
303+
tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()]))
304+
if isinstance(self.__cell__, dict) else id(self.__cell__),
305+
),
297306
self.__owner__,
298307
tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
299308
))

Lib/test/test_annotationlib.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import itertools
99
import pickle
1010
from string.templatelib import Template, Interpolation
11+
import types
1112
import typing
1213
import sys
1314
import unittest
@@ -1862,6 +1863,39 @@ def foo(a: c1_gth, b: c2_gth):
18621863
self.assertNotEqual(hash(c3), hash(c4))
18631864
self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__)))
18641865

1866+
def test_forward_equality_and_hash_with_cells(self):
1867+
"""Regression test for GH-143831."""
1868+
class A:
1869+
def one(_) -> C1:
1870+
"""One cell."""
1871+
1872+
one_f = ForwardRef("C1", owner=one)
1873+
one_f_ga1 = get_annotations(one, format=Format.FORWARDREF)["return"]
1874+
one_f_ga2 = get_annotations(one, format=Format.FORWARDREF)["return"]
1875+
self.assertIsInstance(one_f_ga1.__cell__, types.CellType)
1876+
self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__)
1877+
1878+
def two(_) -> C1 | C2:
1879+
"""Two cells."""
1880+
1881+
two_f_ga1 = get_annotations(two, format=Format.FORWARDREF)["return"]
1882+
two_f_ga2 = get_annotations(two, format=Format.FORWARDREF)["return"]
1883+
self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__)
1884+
self.assertIsInstance(two_f_ga1.__cell__, dict)
1885+
self.assertIsInstance(two_f_ga2.__cell__, dict)
1886+
1887+
type C1 = None
1888+
type C2 = None
1889+
1890+
self.assertNotEqual(A.one_f, A.one_f_ga1)
1891+
self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1))
1892+
1893+
self.assertEqual(A.one_f_ga1, A.one_f_ga2)
1894+
self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2))
1895+
1896+
self.assertEqual(A.two_f_ga1, A.two_f_ga2)
1897+
self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2))
1898+
18651899
def test_forward_equality_namespace(self):
18661900
def namespace1():
18671901
a = ForwardRef("A")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`annotationlib.ForwardRef` objects are now hashable when created from
2+
annotation scopes with closures. Previously, hashing such objects would
3+
throw an exception. Patch by Bartosz Sławecki.

0 commit comments

Comments
 (0)