Skip to content

Commit 3eff46f

Browse files
rfroweplokmijnuhby
andauthored
bpo-37953: Fix ForwardRef hash and equality checks (GH-15400) (GH-18751)
Ideally if we stick a ForwardRef in a dictionary we would like to reliably be able to get it out again. https://bugs.python.org/issue37953 (cherry picked from commit e082e7c) Co-authored-by: plokmijnuhby <[email protected]>
1 parent f8f163c commit 3eff46f

File tree

3 files changed

+123
-3
lines changed

3 files changed

+123
-3
lines changed

Lib/test/test_typing.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,65 @@ def test_forward_equality(self):
15051505
self.assertEqual(fr, typing.ForwardRef('int'))
15061506
self.assertNotEqual(List['int'], List[int])
15071507

1508+
def test_forward_equality_gth(self):
1509+
c1 = typing.ForwardRef('C')
1510+
c1_gth = typing.ForwardRef('C')
1511+
c2 = typing.ForwardRef('C')
1512+
c2_gth = typing.ForwardRef('C')
1513+
1514+
class C:
1515+
pass
1516+
def foo(a: c1_gth, b: c2_gth):
1517+
pass
1518+
1519+
self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': C, 'b': C})
1520+
self.assertEqual(c1, c2)
1521+
self.assertEqual(c1, c1_gth)
1522+
self.assertEqual(c1_gth, c2_gth)
1523+
self.assertEqual(List[c1], List[c1_gth])
1524+
self.assertNotEqual(List[c1], List[C])
1525+
self.assertNotEqual(List[c1_gth], List[C])
1526+
self.assertEquals(Union[c1, c1_gth], Union[c1])
1527+
self.assertEquals(Union[c1, c1_gth, int], Union[c1, int])
1528+
1529+
def test_forward_equality_hash(self):
1530+
c1 = typing.ForwardRef('int')
1531+
c1_gth = typing.ForwardRef('int')
1532+
c2 = typing.ForwardRef('int')
1533+
c2_gth = typing.ForwardRef('int')
1534+
1535+
def foo(a: c1_gth, b: c2_gth):
1536+
pass
1537+
get_type_hints(foo, globals(), locals())
1538+
1539+
self.assertEqual(hash(c1), hash(c2))
1540+
self.assertEqual(hash(c1_gth), hash(c2_gth))
1541+
self.assertEqual(hash(c1), hash(c1_gth))
1542+
1543+
def test_forward_equality_namespace(self):
1544+
class A:
1545+
pass
1546+
def namespace1():
1547+
a = typing.ForwardRef('A')
1548+
def fun(x: a):
1549+
pass
1550+
get_type_hints(fun, globals(), locals())
1551+
return a
1552+
1553+
def namespace2():
1554+
a = typing.ForwardRef('A')
1555+
1556+
class A:
1557+
pass
1558+
def fun(x: a):
1559+
pass
1560+
1561+
get_type_hints(fun, globals(), locals())
1562+
return a
1563+
1564+
self.assertEqual(namespace1(), namespace1())
1565+
self.assertNotEqual(namespace1(), namespace2())
1566+
15081567
def test_forward_repr(self):
15091568
self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]")
15101569

@@ -1524,6 +1583,63 @@ def foo(a: Tuple['T']):
15241583
self.assertEqual(get_type_hints(foo, globals(), locals()),
15251584
{'a': Tuple[T]})
15261585

1586+
def test_forward_recursion_actually(self):
1587+
def namespace1():
1588+
a = typing.ForwardRef('A')
1589+
A = a
1590+
def fun(x: a): pass
1591+
1592+
ret = get_type_hints(fun, globals(), locals())
1593+
return a
1594+
1595+
def namespace2():
1596+
a = typing.ForwardRef('A')
1597+
A = a
1598+
def fun(x: a): pass
1599+
1600+
ret = get_type_hints(fun, globals(), locals())
1601+
return a
1602+
1603+
def cmp(o1, o2):
1604+
return o1 == o2
1605+
1606+
r1 = namespace1()
1607+
r2 = namespace2()
1608+
self.assertIsNot(r1, r2)
1609+
self.assertRaises(RecursionError, cmp, r1, r2)
1610+
1611+
def test_union_forward_recursion(self):
1612+
ValueList = List['Value']
1613+
Value = Union[str, ValueList]
1614+
1615+
class C:
1616+
foo: List[Value]
1617+
class D:
1618+
foo: Union[Value, ValueList]
1619+
class E:
1620+
foo: Union[List[Value], ValueList]
1621+
class F:
1622+
foo: Union[Value, List[Value], ValueList]
1623+
1624+
self.assertEqual(get_type_hints(C, globals(), locals()), get_type_hints(C, globals(), locals()))
1625+
self.assertEqual(get_type_hints(C, globals(), locals()),
1626+
{'foo': List[Union[str, List[Union[str, List['Value']]]]]})
1627+
self.assertEqual(get_type_hints(D, globals(), locals()),
1628+
{'foo': Union[str, List[Union[str, List['Value']]]]})
1629+
self.assertEqual(get_type_hints(E, globals(), locals()),
1630+
{'foo': Union[
1631+
List[Union[str, List[Union[str, List['Value']]]]],
1632+
List[Union[str, List['Value']]]
1633+
]
1634+
})
1635+
self.assertEqual(get_type_hints(F, globals(), locals()),
1636+
{'foo': Union[
1637+
str,
1638+
List[Union[str, List['Value']]],
1639+
List[Union[str, List[Union[str, List['Value']]]]]
1640+
]
1641+
})
1642+
15271643
def test_callable_forward(self):
15281644

15291645
def foo(a: Callable[['T'], 'T']):

Lib/typing.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,11 +473,13 @@ def _evaluate(self, globalns, localns):
473473
def __eq__(self, other):
474474
if not isinstance(other, ForwardRef):
475475
return NotImplemented
476-
return (self.__forward_arg__ == other.__forward_arg__ and
477-
self.__forward_value__ == other.__forward_value__)
476+
if self.__forward_evaluated__ and other.__forward_evaluated__:
477+
return (self.__forward_arg__ == other.__forward_arg__ and
478+
self.__forward_value__ == other.__forward_value__)
479+
return self.__forward_arg__ == other.__forward_arg__
478480

479481
def __hash__(self):
480-
return hash((self.__forward_arg__, self.__forward_value__))
482+
return hash(self.__forward_arg__)
481483

482484
def __repr__(self):
483485
return f'ForwardRef({self.__forward_arg__!r})'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
In :mod:`typing`, improved the ``__hash__`` and ``__eq__`` methods for
2+
:class:`ForwardReferences`.

0 commit comments

Comments
 (0)