[Python-checkins] bpo-37953: Fix ForwardRef hash and equality checks (GH-15400) (GH-18751)

Ryan Rowe webhook-mailer at python.org
Tue Mar 3 17:29:45 EST 2020


https://github.com/python/cpython/commit/3eff46fc7d2e3c80c4dedba4177782f1fc8ad89b
commit: 3eff46fc7d2e3c80c4dedba4177782f1fc8ad89b
branch: 3.7
author: Ryan Rowe <ryanf.rowe at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-03-03T17:29:40-05:00
summary:

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 <39633434+plokmijnuhby at users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 087dc2a82f038..6e7e5a210a658 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -1505,6 +1505,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')]")
 
@@ -1524,6 +1583,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']):
diff --git a/Lib/typing.py b/Lib/typing.py
index 3b2e3720fff57..1749c2fd35cd1 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -473,11 +473,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})'
diff --git a/Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst b/Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst
new file mode 100644
index 0000000000000..4eff4f7479aad
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst
@@ -0,0 +1,2 @@
+In :mod:`typing`, improved the ``__hash__`` and ``__eq__`` methods for
+:class:`ForwardReferences`.



More information about the Python-checkins mailing list