Skip to content

Commit e082e7c

Browse files
plokmijnuhbyilevkivskyi
authored andcommitted
bpo-37953: Fix ForwardRef hash and equality checks (GH-15400)
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
1 parent 77cd0ce commit e082e7c

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
@@ -2361,6 +2361,65 @@ def test_forward_equality(self):
23612361
self.assertEqual(fr, typing.ForwardRef('int'))
23622362
self.assertNotEqual(List['int'], List[int])
23632363

2364+
def test_forward_equality_gth(self):
2365+
c1 = typing.ForwardRef('C')
2366+
c1_gth = typing.ForwardRef('C')
2367+
c2 = typing.ForwardRef('C')
2368+
c2_gth = typing.ForwardRef('C')
2369+
2370+
class C:
2371+
pass
2372+
def foo(a: c1_gth, b: c2_gth):
2373+
pass
2374+
2375+
self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': C, 'b': C})
2376+
self.assertEqual(c1, c2)
2377+
self.assertEqual(c1, c1_gth)
2378+
self.assertEqual(c1_gth, c2_gth)
2379+
self.assertEqual(List[c1], List[c1_gth])
2380+
self.assertNotEqual(List[c1], List[C])
2381+
self.assertNotEqual(List[c1_gth], List[C])
2382+
self.assertEquals(Union[c1, c1_gth], Union[c1])
2383+
self.assertEquals(Union[c1, c1_gth, int], Union[c1, int])
2384+
2385+
def test_forward_equality_hash(self):
2386+
c1 = typing.ForwardRef('int')
2387+
c1_gth = typing.ForwardRef('int')
2388+
c2 = typing.ForwardRef('int')
2389+
c2_gth = typing.ForwardRef('int')
2390+
2391+
def foo(a: c1_gth, b: c2_gth):
2392+
pass
2393+
get_type_hints(foo, globals(), locals())
2394+
2395+
self.assertEqual(hash(c1), hash(c2))
2396+
self.assertEqual(hash(c1_gth), hash(c2_gth))
2397+
self.assertEqual(hash(c1), hash(c1_gth))
2398+
2399+
def test_forward_equality_namespace(self):
2400+
class A:
2401+
pass
2402+
def namespace1():
2403+
a = typing.ForwardRef('A')
2404+
def fun(x: a):
2405+
pass
2406+
get_type_hints(fun, globals(), locals())
2407+
return a
2408+
2409+
def namespace2():
2410+
a = typing.ForwardRef('A')
2411+
2412+
class A:
2413+
pass
2414+
def fun(x: a):
2415+
pass
2416+
2417+
get_type_hints(fun, globals(), locals())
2418+
return a
2419+
2420+
self.assertEqual(namespace1(), namespace1())
2421+
self.assertNotEqual(namespace1(), namespace2())
2422+
23642423
def test_forward_repr(self):
23652424
self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]")
23662425

@@ -2380,6 +2439,63 @@ def foo(a: Tuple['T']):
23802439
self.assertEqual(get_type_hints(foo, globals(), locals()),
23812440
{'a': Tuple[T]})
23822441

2442+
def test_forward_recursion_actually(self):
2443+
def namespace1():
2444+
a = typing.ForwardRef('A')
2445+
A = a
2446+
def fun(x: a): pass
2447+
2448+
ret = get_type_hints(fun, globals(), locals())
2449+
return a
2450+
2451+
def namespace2():
2452+
a = typing.ForwardRef('A')
2453+
A = a
2454+
def fun(x: a): pass
2455+
2456+
ret = get_type_hints(fun, globals(), locals())
2457+
return a
2458+
2459+
def cmp(o1, o2):
2460+
return o1 == o2
2461+
2462+
r1 = namespace1()
2463+
r2 = namespace2()
2464+
self.assertIsNot(r1, r2)
2465+
self.assertRaises(RecursionError, cmp, r1, r2)
2466+
2467+
def test_union_forward_recursion(self):
2468+
ValueList = List['Value']
2469+
Value = Union[str, ValueList]
2470+
2471+
class C:
2472+
foo: List[Value]
2473+
class D:
2474+
foo: Union[Value, ValueList]
2475+
class E:
2476+
foo: Union[List[Value], ValueList]
2477+
class F:
2478+
foo: Union[Value, List[Value], ValueList]
2479+
2480+
self.assertEqual(get_type_hints(C, globals(), locals()), get_type_hints(C, globals(), locals()))
2481+
self.assertEqual(get_type_hints(C, globals(), locals()),
2482+
{'foo': List[Union[str, List[Union[str, List['Value']]]]]})
2483+
self.assertEqual(get_type_hints(D, globals(), locals()),
2484+
{'foo': Union[str, List[Union[str, List['Value']]]]})
2485+
self.assertEqual(get_type_hints(E, globals(), locals()),
2486+
{'foo': Union[
2487+
List[Union[str, List[Union[str, List['Value']]]]],
2488+
List[Union[str, List['Value']]]
2489+
]
2490+
})
2491+
self.assertEqual(get_type_hints(F, globals(), locals()),
2492+
{'foo': Union[
2493+
str,
2494+
List[Union[str, List['Value']]],
2495+
List[Union[str, List[Union[str, List['Value']]]]]
2496+
]
2497+
})
2498+
23832499
def test_callable_forward(self):
23842500

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

Lib/typing.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,11 +524,13 @@ def _evaluate(self, globalns, localns):
524524
def __eq__(self, other):
525525
if not isinstance(other, ForwardRef):
526526
return NotImplemented
527-
return (self.__forward_arg__ == other.__forward_arg__ and
528-
self.__forward_value__ == other.__forward_value__)
527+
if self.__forward_evaluated__ and other.__forward_evaluated__:
528+
return (self.__forward_arg__ == other.__forward_arg__ and
529+
self.__forward_value__ == other.__forward_value__)
530+
return self.__forward_arg__ == other.__forward_arg__
529531

530532
def __hash__(self):
531-
return hash((self.__forward_arg__, self.__forward_value__))
533+
return hash(self.__forward_arg__)
532534

533535
def __repr__(self):
534536
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)