From e820e3a776ac25cc5ba371b7f5bed78755eba64f Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Tue, 25 Jan 2022 23:11:15 +0100 Subject: [PATCH 1/9] convert strings in PEP 585 generic aliases (`types.GenericAlias`) to `ForwardRef` before evaluation (https://bugs.python.org/issue41370) --- Lib/typing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index e3e098b1fcc8f3..ac56b545b49f6c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -331,6 +331,12 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): if isinstance(t, ForwardRef): return t._evaluate(globalns, localns, recursive_guard) if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): + if isinstance(t, GenericAlias): + args = tuple( + ForwardRef(arg) if isinstance(arg, str) else arg + for arg in t.__args__ + ) + t = t.__origin__[(*args,)] ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) if ev_args == t.__args__: return t From d7f57c7eb10ce94e9b6d32bbab92277e22420d8b Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Tue, 25 Jan 2022 23:29:34 +0100 Subject: [PATCH 2/9] add unittest for new get_type_hints() behaviour on PEP 585 generics with forward refs --- Lib/test/test_typing.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5777656552d797..b94b7aae13ba71 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2006,6 +2006,23 @@ def barfoo(x: AT): ... def barfoo2(x: CT): ... self.assertIs(get_type_hints(barfoo2, globals(), locals())['x'], CT) + def test_generic_pep585_forward_ref(self): + # See https://bugs.python.org/issue41370 + + class N: + a: list['N'] + self.assertEqual( + get_type_hints(N, globals(), locals()), + {'a': list[N]} + ) + + class M: + a: dict['N', list[List[list['M']]]] + self.assertEqual( + get_type_hints(M, globals(), locals()), + {'a': dict[N, list[List[list[M]]]]} + ) + def test_extended_generic_rules_subclassing(self): class T1(Tuple[T, KT]): ... class T2(Tuple[T, ...]): ... From 26ab74013e8274457724eba1a9e83a98a719176d Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 27 Jan 2022 12:44:10 +0100 Subject: [PATCH 3/9] add unit test for `from __future__ import annotations` --- Lib/test/test_typing.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b94b7aae13ba71..30d5542af7e17f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -30,6 +30,7 @@ from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs from typing import TypeGuard import abc +import textwrap import typing import weakref import types @@ -2009,18 +2010,32 @@ def barfoo2(x: CT): ... def test_generic_pep585_forward_ref(self): # See https://bugs.python.org/issue41370 - class N: - a: list['N'] + class C1: + a: list['C1'] + self.assertEqual( + get_type_hints(C1, globals(), locals()), + {'a': list[C1]} + ) + + class C2: + a: dict['C1', list[List[list['C2']]]] self.assertEqual( - get_type_hints(N, globals(), locals()), - {'a': list[N]} + get_type_hints(C2, globals(), locals()), + {'a': dict[C1, list[List[list[C2]]]]} ) - class M: - a: dict['N', list[List[list['M']]]] + scope = {} + exec(textwrap.dedent(''' + from __future__ import annotations + class C3: + a: List[list["C2"]] + '''), scope) + C3 = scope['C3'] + + self.assertEqual(C3.__annotations__['a'], "List[list['C2']]") self.assertEqual( - get_type_hints(M, globals(), locals()), - {'a': dict[N, list[List[list[M]]]]} + get_type_hints(C3, globals(), locals()), + {'a': List[list[C2]]} ) def test_extended_generic_rules_subclassing(self): From a04bd5d8867f71f0ba2497a0ef65f8d8b8833959 Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 27 Jan 2022 12:47:35 +0100 Subject: [PATCH 4/9] add test for recursive type --- Lib/test/test_typing.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 30d5542af7e17f..409ee141d3c1a4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2024,6 +2024,7 @@ class C2: {'a': dict[C1, list[List[list[C2]]]]} ) + # Test stringified annotations scope = {} exec(textwrap.dedent(''' from __future__ import annotations @@ -2031,13 +2032,20 @@ class C3: a: List[list["C2"]] '''), scope) C3 = scope['C3'] - self.assertEqual(C3.__annotations__['a'], "List[list['C2']]") self.assertEqual( get_type_hints(C3, globals(), locals()), {'a': List[list[C2]]} ) + # Test recursive types + X = list["X"] + def f(x: X): ... + self.assertEqual( + get_type_hints(f, globals(), locals()), + {'x': list[list[ForwardRef('X')]]} + ) + def test_extended_generic_rules_subclassing(self): class T1(Tuple[T, KT]): ... class T2(Tuple[T, ...]): ... From 3416713dd4c885c942f342190ca103ade254eb25 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 27 Jan 2022 11:54:17 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst diff --git a/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst new file mode 100644 index 00000000000000..193aaea304b7b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst @@ -0,0 +1 @@ +`typing.get_type_hints()` now supports evaluating strings as forward references in PEP 585 generic aliases. \ No newline at end of file From 30a3564f176bb1b1c0a2931fc78fe25ad9f88710 Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 27 Jan 2022 03:56:10 -0800 Subject: [PATCH 6/9] Double backticks because RST. --- .../next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst index 193aaea304b7b4..83624504d9c768 100644 --- a/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst +++ b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst @@ -1 +1 @@ -`typing.get_type_hints()` now supports evaluating strings as forward references in PEP 585 generic aliases. \ No newline at end of file +``typing.get_type_hints()`` now supports evaluating strings as forward references in PEP 585 generic aliases. From 13a350520b239da22a4e4f5d33da407ba70589f5 Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 27 Jan 2022 12:56:21 +0100 Subject: [PATCH 7/9] Update Lib/typing.py Co-authored-by: Jelle Zijlstra --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index ac56b545b49f6c..373c5268fad8f5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -336,7 +336,7 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): ForwardRef(arg) if isinstance(arg, str) else arg for arg in t.__args__ ) - t = t.__origin__[(*args,)] + t = t.__origin__[args] ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) if ev_args == t.__args__: return t From 33d286a3728b234dd89485e81da8a1daaa5f00a9 Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 27 Jan 2022 13:18:58 +0100 Subject: [PATCH 8/9] Update Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst Co-authored-by: Alex Waygood --- .../next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst index 83624504d9c768..d9ad2af156a4dd 100644 --- a/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst +++ b/Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst @@ -1 +1 @@ -``typing.get_type_hints()`` now supports evaluating strings as forward references in PEP 585 generic aliases. +:func:`typing.get_type_hints` now supports evaluating strings as forward references in :ref:`PEP 585 generic aliases `. From 39c5b0641acb8ab55a09712bb5a65075c7e9ccd6 Mon Sep 17 00:00:00 2001 From: Niklas Rosenstein Date: Thu, 3 Mar 2022 08:18:23 -0800 Subject: [PATCH 9/9] Replace assertIs with assertEqual in test_get_type_hints_annotated --- Lib/test/test_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 409ee141d3c1a4..13efb549d89d80 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3310,7 +3310,7 @@ def foobar(x: list[ForwardRef('X')]): ... BA = Tuple[Annotated[T, (1, 0)], ...] def barfoo(x: BA): ... self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], Tuple[T, ...]) - self.assertIs( + self.assertEqual( get_type_hints(barfoo, globals(), locals(), include_extras=True)['x'], BA ) @@ -3318,7 +3318,7 @@ def barfoo(x: BA): ... BA = tuple[Annotated[T, (1, 0)], ...] def barfoo(x: BA): ... self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], tuple[T, ...]) - self.assertIs( + self.assertEqual( get_type_hints(barfoo, globals(), locals(), include_extras=True)['x'], BA )