Skip to content

Commit

Permalink
bpo-37953: Fix ForwardRef hash and equality checks (GH-15400)
Browse files Browse the repository at this point in the history
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 <39633434+plokmijnuhby@users.noreply.github.com>
  • Loading branch information
miss-islington and plokmijnuhby committed Sep 13, 2019
1 parent cd85200 commit e91edfe
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 3 deletions.
116 changes: 116 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,65 @@ def test_forward_equality(self):
self.assertEqual(fr, typing.ForwardRef('int'))
self.assertNotEqual(List['int'], List[int])

def test_forward_equality_gth(self):
c1 = typing.ForwardRef('C')
c1_gth = typing.ForwardRef('C')
c2 = typing.ForwardRef('C')
c2_gth = typing.ForwardRef('C')

class C:
pass
def foo(a: c1_gth, b: c2_gth):
pass

self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': C, 'b': C})
self.assertEqual(c1, c2)
self.assertEqual(c1, c1_gth)
self.assertEqual(c1_gth, c2_gth)
self.assertEqual(List[c1], List[c1_gth])
self.assertNotEqual(List[c1], List[C])
self.assertNotEqual(List[c1_gth], List[C])
self.assertEquals(Union[c1, c1_gth], Union[c1])
self.assertEquals(Union[c1, c1_gth, int], Union[c1, int])

def test_forward_equality_hash(self):
c1 = typing.ForwardRef('int')
c1_gth = typing.ForwardRef('int')
c2 = typing.ForwardRef('int')
c2_gth = typing.ForwardRef('int')

def foo(a: c1_gth, b: c2_gth):
pass
get_type_hints(foo, globals(), locals())

self.assertEqual(hash(c1), hash(c2))
self.assertEqual(hash(c1_gth), hash(c2_gth))
self.assertEqual(hash(c1), hash(c1_gth))

def test_forward_equality_namespace(self):
class A:
pass
def namespace1():
a = typing.ForwardRef('A')
def fun(x: a):
pass
get_type_hints(fun, globals(), locals())
return a

def namespace2():
a = typing.ForwardRef('A')

class A:
pass
def fun(x: a):
pass

get_type_hints(fun, globals(), locals())
return a

self.assertEqual(namespace1(), namespace1())
self.assertNotEqual(namespace1(), namespace2())

def test_forward_repr(self):
self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]")

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

def test_forward_recursion_actually(self):
def namespace1():
a = typing.ForwardRef('A')
A = a
def fun(x: a): pass

ret = get_type_hints(fun, globals(), locals())
return a

def namespace2():
a = typing.ForwardRef('A')
A = a
def fun(x: a): pass

ret = get_type_hints(fun, globals(), locals())
return a

def cmp(o1, o2):
return o1 == o2

r1 = namespace1()
r2 = namespace2()
self.assertIsNot(r1, r2)
self.assertRaises(RecursionError, cmp, r1, r2)

def test_union_forward_recursion(self):
ValueList = List['Value']
Value = Union[str, ValueList]

class C:
foo: List[Value]
class D:
foo: Union[Value, ValueList]
class E:
foo: Union[List[Value], ValueList]
class F:
foo: Union[Value, List[Value], ValueList]

self.assertEqual(get_type_hints(C, globals(), locals()), get_type_hints(C, globals(), locals()))
self.assertEqual(get_type_hints(C, globals(), locals()),
{'foo': List[Union[str, List[Union[str, List['Value']]]]]})
self.assertEqual(get_type_hints(D, globals(), locals()),
{'foo': Union[str, List[Union[str, List['Value']]]]})
self.assertEqual(get_type_hints(E, globals(), locals()),
{'foo': Union[
List[Union[str, List[Union[str, List['Value']]]]],
List[Union[str, List['Value']]]
]
})
self.assertEqual(get_type_hints(F, globals(), locals()),
{'foo': Union[
str,
List[Union[str, List['Value']]],
List[Union[str, List[Union[str, List['Value']]]]]
]
})

def test_callable_forward(self):

def foo(a: Callable[['T'], 'T']):
Expand Down
8 changes: 5 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,11 +524,13 @@ def _evaluate(self, globalns, localns):
def __eq__(self, other):
if not isinstance(other, ForwardRef):
return NotImplemented
return (self.__forward_arg__ == other.__forward_arg__ and
self.__forward_value__ == other.__forward_value__)
if self.__forward_evaluated__ and other.__forward_evaluated__:
return (self.__forward_arg__ == other.__forward_arg__ and
self.__forward_value__ == other.__forward_value__)
return self.__forward_arg__ == other.__forward_arg__

def __hash__(self):
return hash((self.__forward_arg__, self.__forward_value__))
return hash(self.__forward_arg__)

def __repr__(self):
return f'ForwardRef({self.__forward_arg__!r})'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
In :mod:`typing`, improved the ``__hash__`` and ``__eq__`` methods for
:class:`ForwardReferences`.

0 comments on commit e91edfe

Please sign in to comment.