From ac5fd652091eed1ff89e5db50a3ab84b88e5a6c0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 8 Jun 2024 15:30:17 +0100 Subject: [PATCH 1/5] gh-114053: Fix bad interaction of PEP 695, PEP 563 and `inspect.get_annotations` --- Lib/inspect.py | 8 ++++- .../inspect_stringized_annotations_pep695.py | 23 +++++++++++++ Lib/test/test_inspect/test_inspect.py | 33 +++++++++++++++++++ ...-06-08-15-15-29.gh-issue-114053.WQLAFG.rst | 4 +++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_inspect/inspect_stringized_annotations_pep695.py create mode 100644 Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 2b7f8bec482f8e..1eb2b35bd9a2d3 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -280,7 +280,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False): if globals is None: globals = obj_globals if locals is None: - locals = obj_locals + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals return_value = {key: value if not isinstance(value, str) else eval(value, globals, locals) diff --git a/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py new file mode 100644 index 00000000000000..c6614d17e1471b --- /dev/null +++ b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py @@ -0,0 +1,23 @@ + +from __future__ import annotations +from typing import Callable, Unpack + + +class A[T, *Ts, **P]: + x: T + y: tuple[*Ts] + z: Callable[P, str] + + +class B[T, *Ts, **P]: + T = int + Ts = str + P = bytes + x: T + y: Ts + z: P + + +def generic_function[T, *Ts, **P]( + x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs +) -> None: ... diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 0a4fa9343f15e0..7504e7efeff7dd 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -47,6 +47,7 @@ from test.test_inspect import inspect_stock_annotations from test.test_inspect import inspect_stringized_annotations from test.test_inspect import inspect_stringized_annotations_2 +from test.test_inspect import inspect_stringized_annotations_pep695 # Functions tested in this suite: @@ -1692,6 +1693,38 @@ def wrapper(a, b): self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) + def test_get_annotations_with_stringized_pep695_annotations(self): + from typing import Unpack + ann_module695 = inspect_stringized_annotations_pep695 + + A_annotations = inspect.get_annotations(ann_module695.A, eval_str=True) + A_type_params = ann_module695.A.__type_params__ + self.assertIs(A_annotations["x"], A_type_params[0]) + self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]]) + self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) + + B_annotations = inspect.get_annotations(ann_module695.B, eval_str=True) + self.assertEqual(B_annotations.keys(), {"x", "y", "z"}) + self.assertEqual( + set(B_annotations.values()).intersection(ann_module695.B.__type_params__), + set() + ) + + generic_function_annotations = inspect.get_annotations( + ann_module695.generic_function, eval_str=True + ) + func_t_params = ann_module695.generic_function.__type_params__ + self.assertEqual( + generic_function_annotations.keys(), {"x", "y", "z", "zz", "return"} + ) + self.assertIs(generic_function_annotations["x"], func_t_params[0]) + self.assertEqual( + generic_function_annotations["y"], + Unpack[func_t_params[1]] + ) + self.assertIs(generic_function_annotations["z"].__origin__, func_t_params[2]) + self.assertIs(generic_function_annotations["zz"].__origin__, func_t_params[2]) + class TestFormatAnnotation(unittest.TestCase): def test_typing_replacement(self): diff --git a/Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst b/Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst new file mode 100644 index 00000000000000..be49577a712867 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst @@ -0,0 +1,4 @@ +Fix erroneous :exc:`NameError` when calling :func:`inspect.get_annotations` +with ``eval_str=True``` on a class that made use of :pep:`695` type +parameters in a module that had ``from __future__ import annotations`` at +the top of the file. Patch by Alex Waygood. From 99c5b440323118596cac719cc0d50b180c2742de Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 8 Jun 2024 15:41:28 +0100 Subject: [PATCH 2/5] don't try to be so clever in the test --- Lib/test/test_inspect/test_inspect.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 7504e7efeff7dd..8cd4cec8d4ea4d 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1704,11 +1704,7 @@ def test_get_annotations_with_stringized_pep695_annotations(self): self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) B_annotations = inspect.get_annotations(ann_module695.B, eval_str=True) - self.assertEqual(B_annotations.keys(), {"x", "y", "z"}) - self.assertEqual( - set(B_annotations.values()).intersection(ann_module695.B.__type_params__), - set() - ) + self.assertEqual(B_annotations, {"x": int, "y": str, "P": bytes}) generic_function_annotations = inspect.get_annotations( ann_module695.generic_function, eval_str=True From d3a3233b0915e36f871dfdb151232f9120cab9bb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 8 Jun 2024 15:43:32 +0100 Subject: [PATCH 3/5] Update test_inspect.py --- Lib/test/test_inspect/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 8cd4cec8d4ea4d..847968f93e8f26 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1704,7 +1704,7 @@ def test_get_annotations_with_stringized_pep695_annotations(self): self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) B_annotations = inspect.get_annotations(ann_module695.B, eval_str=True) - self.assertEqual(B_annotations, {"x": int, "y": str, "P": bytes}) + self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes}) generic_function_annotations = inspect.get_annotations( ann_module695.generic_function, eval_str=True From 99079c6efb24a9dd62ed3f506f28b67e2f0ea929 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 13 Jun 2024 21:49:52 +0100 Subject: [PATCH 4/5] Add the beefed-up tests from the other PR --- .../inspect_stringized_annotations_pep695.py | 52 +++++++++- Lib/test/test_inspect/test_inspect.py | 96 ++++++++++++++++--- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py index c6614d17e1471b..bb50fe69a7df6f 100644 --- a/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py +++ b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py @@ -1,5 +1,5 @@ - from __future__ import annotations +from inspect import get_annotations from typing import Callable, Unpack @@ -18,6 +18,56 @@ class B[T, *Ts, **P]: z: P +Eggs = int +Spam = str + + +class C[Eggs, **Spam]: + x: Eggs + y: Spam + + def generic_function[T, *Ts, **P]( x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs ) -> None: ... + + +def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass + + +class D: + Foo = int + Bar = str + + def generic_method[Foo, **Bar]( + self, x: Foo, y: Bar + ) -> None: ... + + def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + +def nested(): + from types import SimpleNamespace + from inspect import get_annotations + + Eggs = bytes + Spam = memoryview + + + class E[Eggs, **Spam]: + x: Eggs + y: Spam + + def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + + def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass + + + return SimpleNamespace( + E=E, + E_annotations=get_annotations(E, eval_str=True), + E_meth_annotations=get_annotations(E.generic_method, eval_str=True), + generic_func=generic_function, + generic_func_annotations=get_annotations(generic_function, eval_str=True) + ) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 847968f93e8f26..140efac530afb2 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -22,6 +22,7 @@ import types import tempfile import textwrap +from typing import Unpack import unicodedata import unittest import unittest.mock @@ -1693,33 +1694,106 @@ def wrapper(a, b): self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) - def test_get_annotations_with_stringized_pep695_annotations(self): - from typing import Unpack + def test_pep695_generic_class_with_future_annotations(self): ann_module695 = inspect_stringized_annotations_pep695 - A_annotations = inspect.get_annotations(ann_module695.A, eval_str=True) A_type_params = ann_module695.A.__type_params__ self.assertIs(A_annotations["x"], A_type_params[0]) self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]]) self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) - B_annotations = inspect.get_annotations(ann_module695.B, eval_str=True) + def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self): + B_annotations = inspect.get_annotations( + inspect_stringized_annotations_pep695.B, eval_str=True + ) self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes}) - generic_function_annotations = inspect.get_annotations( + def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self): + ann_module695 = inspect_stringized_annotations_pep695 + C_annotations = inspect.get_annotations(ann_module695.C, eval_str=True) + self.assertEqual( + set(C_annotations.values()), + set(ann_module695.C.__type_params__) + ) + + def test_pep_695_generic_function_with_future_annotations(self): + ann_module695 = inspect_stringized_annotations_pep695 + generic_func_annotations = inspect.get_annotations( ann_module695.generic_function, eval_str=True ) func_t_params = ann_module695.generic_function.__type_params__ self.assertEqual( - generic_function_annotations.keys(), {"x", "y", "z", "zz", "return"} + generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"} + ) + self.assertIs(generic_func_annotations["x"], func_t_params[0]) + self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]]) + self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2]) + self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2]) + + def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + inspect.get_annotations( + inspect_stringized_annotations_pep695.generic_function_2, + eval_str=True + ).values() + ), + set( + inspect_stringized_annotations_pep695.generic_function_2.__type_params__ + ) + ) + + def test_pep_695_generic_method_with_future_annotations(self): + ann_module695 = inspect_stringized_annotations_pep695 + generic_method_annotations = inspect.get_annotations( + ann_module695.D.generic_method, eval_str=True + ) + params = { + param.__name__: param + for param in ann_module695.D.generic_method.__type_params__ + } + self.assertEqual( + generic_method_annotations, + {"x": params["Foo"], "y": params["Bar"], "return": None} + ) + + def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + inspect.get_annotations( + inspect_stringized_annotations_pep695.D.generic_method_2, + eval_str=True + ).values() + ), + set( + inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__ + ) + ) + + def test_pep_695_generics_with_future_annotations_nested_in_function(self): + results = inspect_stringized_annotations_pep695.nested() + + self.assertEqual( + set(results.E_annotations.values()), + set(results.E.__type_params__) + ) + self.assertEqual( + set(results.E_meth_annotations.values()), + set(results.E.generic_method.__type_params__) + ) + self.assertNotEqual( + set(results.E_meth_annotations.values()), + set(results.E.__type_params__) + ) + self.assertEqual( + set(results.E_meth_annotations.values()).intersection(results.E.__type_params__), + set() ) - self.assertIs(generic_function_annotations["x"], func_t_params[0]) + self.assertEqual( - generic_function_annotations["y"], - Unpack[func_t_params[1]] + set(results.generic_func_annotations.values()), + set(results.generic_func.__type_params__) ) - self.assertIs(generic_function_annotations["z"].__origin__, func_t_params[2]) - self.assertIs(generic_function_annotations["zz"].__origin__, func_t_params[2]) class TestFormatAnnotation(unittest.TestCase): From bb3b84b76c8c1922bd56a7305fd9a5e361bb8ff2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 13 Jun 2024 21:51:41 +0100 Subject: [PATCH 5/5] fix lint --- Lib/test/test_inspect/inspect_stringized_annotations_pep695.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py index bb50fe69a7df6f..723822f8eaa92d 100644 --- a/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py +++ b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py @@ -1,5 +1,4 @@ from __future__ import annotations -from inspect import get_annotations from typing import Callable, Unpack