From b9ac6513190023d118e2e3f029ca231540b79bf2 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Thu, 22 Aug 2019 20:44:41 +0100 Subject: [PATCH 01/11] Update typing.py --- Lib/typing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 5f1a0ad3d63743..3164d7acd5eebd 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -486,7 +486,7 @@ def open_helper(file: str, mode: MODE) -> str: """) -class ForwardRef(_Final, _root=True): +class ForwardRef(_Final, _Immutable, _root=True): """Internal wrapper to hold a forward reference.""" __slots__ = ('__forward_arg__', '__forward_code__', @@ -524,11 +524,10 @@ 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__) + 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})' From 0782452dcca692a802ef78511c42aa815d0d9a42 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Thu, 22 Aug 2019 20:56:10 +0100 Subject: [PATCH 02/11] Update typing.py Maybe this will cut down on hash collisions --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 3164d7acd5eebd..174f73b3e18603 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -527,7 +527,7 @@ def __eq__(self, other): return self.__forward_arg__ == other.__forward_arg__ def __hash__(self): - return hash(self.__forward_arg__) + return hash((self.__forward_arg__,)) def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' From 32737b602d242983caa36da66c5872d0b3cce42b Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Tue, 27 Aug 2019 20:17:29 +0100 Subject: [PATCH 03/11] Added unit test --- Lib/test/test_typing.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ba001c3462fcf5..a09b969a02fb13 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2734,6 +2734,17 @@ def test_get_type_hints_ClassVar(self): {'z': ClassVar[CSub], 'y': int, 'b': int, 'x': ClassVar[Optional[B]]}) self.assertEqual(gth(G), {'lst': ClassVar[List[T]]}) + + def test_get_type_hints_retains_forward_equality(self): + fr = typing.ForwardRef('int') + hsh = hash(fr) + + def foo(a: fr): + pass + + gth(foo) + self.assertEqual(fr, typing.ForwardRef('int')) + self.assertEqual(hash(fr), hsh) class GetUtilitiesTestCase(TestCase): From fbaf6aa529c371e4fee8f1d5859bb424cb448af0 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Tue, 3 Sep 2019 21:05:21 +0100 Subject: [PATCH 04/11] Update Lib/typing.py Remove the tuple in forwardref hash Co-Authored-By: Brandt Bucher --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 174f73b3e18603..3164d7acd5eebd 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -527,7 +527,7 @@ def __eq__(self, other): return self.__forward_arg__ == other.__forward_arg__ def __hash__(self): - return hash((self.__forward_arg__,)) + return hash(self.__forward_arg__) def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' From 344db8832dc65acbbfd763a31d4f1c04d65f316c Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Tue, 3 Sep 2019 21:09:02 +0100 Subject: [PATCH 05/11] Update Lib/typing.py Alright, I don't really agree with this but I seem to be in the minority, so I'll change it. Co-Authored-By: Brandt Bucher --- Lib/typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 3164d7acd5eebd..df6ab902f90ecb 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -524,6 +524,9 @@ def _evaluate(self, globalns, localns): def __eq__(self, other): if not isinstance(other, ForwardRef): return NotImplemented + 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): From 725baa13123db2e088c5d6ca06a0c0d1e2dc574a Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 17:40:36 +0000 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst 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 00000000000000..d3dbf33eb4506b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst @@ -0,0 +1,3 @@ +Forward references in the typing module now have different hash and eq methods. References will now compare equal properly if only one of them has been evaluated and the other hasn't. Hash values are now consistent across the lifetime of a forward reference. + +There is a slight chance this will break existing code, but this has been judged to be unlikely. If two references have the same hash, this now does not mean they are equal, although if two references are equal they will have the same hash. \ No newline at end of file From 1cf37233c9d70a40cef128161bdce27a8d3f216f Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:03:38 +0100 Subject: [PATCH 07/11] Update typing.py --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index df6ab902f90ecb..25ea9f2958997c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -486,7 +486,7 @@ def open_helper(file: str, mode: MODE) -> str: """) -class ForwardRef(_Final, _Immutable, _root=True): +class ForwardRef(_Final, _root=True): """Internal wrapper to hold a forward reference.""" __slots__ = ('__forward_arg__', '__forward_code__', From c7ca133dd082c9ca1d5404535b392336e2bc76c4 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:44:11 +0100 Subject: [PATCH 08/11] Update 2019-09-06-17-40-34.bpo-37953.db5FQq.rst --- .../next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index d3dbf33eb4506b..beaf98c1905dca 100644 --- 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 @@ -1,3 +1 @@ -Forward references in the typing module now have different hash and eq methods. References will now compare equal properly if only one of them has been evaluated and the other hasn't. Hash values are now consistent across the lifetime of a forward reference. - -There is a slight chance this will break existing code, but this has been judged to be unlikely. If two references have the same hash, this now does not mean they are equal, although if two references are equal they will have the same hash. \ No newline at end of file +Forward references in the typing module now have improved __hash__ and __eq__ methods. From 3dfd896be1e312772ecb2c234324148436d465dc Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:59:46 +0100 Subject: [PATCH 09/11] Update test_typing.py Add extra tests from pull request #15650. --- Lib/test/test_typing.py | 128 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a09b969a02fb13..45b44bf50595a3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2336,6 +2336,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')]") @@ -2355,6 +2414,64 @@ 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 + # class A: pass + 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() + assert r1 is not 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']): @@ -2734,17 +2851,6 @@ def test_get_type_hints_ClassVar(self): {'z': ClassVar[CSub], 'y': int, 'b': int, 'x': ClassVar[Optional[B]]}) self.assertEqual(gth(G), {'lst': ClassVar[List[T]]}) - - def test_get_type_hints_retains_forward_equality(self): - fr = typing.ForwardRef('int') - hsh = hash(fr) - - def foo(a: fr): - pass - - gth(foo) - self.assertEqual(fr, typing.ForwardRef('int')) - self.assertEqual(hash(fr), hsh) class GetUtilitiesTestCase(TestCase): From c9829df2188ea9213c4799203697e1598dceee87 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Fri, 13 Sep 2019 11:19:24 +0100 Subject: [PATCH 10/11] Update test_typing.py --- Lib/test/test_typing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 45b44bf50595a3..85b274e8be4ecf 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2426,7 +2426,6 @@ def fun(x: a): pass def namespace2(): a = typing.ForwardRef('A') A = a - # class A: pass def fun(x: a): pass ret = get_type_hints(fun, globals(), locals()) @@ -2437,7 +2436,7 @@ def cmp(o1, o2): r1 = namespace1() r2 = namespace2() - assert r1 is not r2 + self.assertIsNot(r1, r2) self.assertRaises(RecursionError, cmp, r1, r2) def test_union_forward_recursion(self): From dd2ca7a567c84b81a33b33d13c70f409b03074d3 Mon Sep 17 00:00:00 2001 From: plokmijnuhby <39633434+plokmijnuhby@users.noreply.github.com> Date: Fri, 13 Sep 2019 11:20:32 +0100 Subject: [PATCH 11/11] Update Misc/NEWS.d/next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst Co-Authored-By: Kyle Stanley --- .../next/Library/2019-09-06-17-40-34.bpo-37953.db5FQq.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index beaf98c1905dca..4eff4f7479aade 100644 --- 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 @@ -1 +1,2 @@ -Forward references in the typing module now have improved __hash__ and __eq__ methods. +In :mod:`typing`, improved the ``__hash__`` and ``__eq__`` methods for +:class:`ForwardReferences`.