]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-114053: Fix bad interaction of PEP 695, PEP 563 and `inspect.get_annotation...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 13 Jun 2024 21:41:14 +0000 (23:41 +0200)
committerGitHub <noreply@github.com>
Thu, 13 Jun 2024 21:41:14 +0000 (21:41 +0000)
gh-114053: Fix bad interaction of PEP 695, PEP 563 and `inspect.get_annotations` (GH-120270)
(cherry picked from commit 42351c3b9a357ec67135b30ed41f59e6f306ac52)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Lib/inspect.py
Lib/test/test_inspect/inspect_stringized_annotations_pep695.py [new file with mode: 0644]
Lib/test/test_inspect/test_inspect.py
Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst [new file with mode: 0644]

index 2b7f8bec482f8eb0c1ec5e9113bc90bfcce07357..1eb2b35bd9a2d3740cbdae32c865d8be7524e483 100644 (file)
@@ -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 (file)
index 0000000..723822f
--- /dev/null
@@ -0,0 +1,72 @@
+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
+
+
+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)
+    )
index 5adccf531dbf009b0a6c4c4a95255e7d61aa2ed1..335dda66aba4cb9b75bf129276d84bc83aaea09e 100644 (file)
@@ -22,6 +22,7 @@ import time
 import types
 import tempfile
 import textwrap
+from typing import Unpack
 import unicodedata
 import unittest
 import unittest.mock
@@ -47,6 +48,7 @@ from test.test_inspect import inspect_fodder2 as mod2
 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 +1694,107 @@ class TestClassesAndFunctions(unittest.TestCase):
         self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'})
         self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
 
+    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])
+
+    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})
+
+    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_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.assertEqual(
+            set(results.generic_func_annotations.values()),
+            set(results.generic_func.__type_params__)
+        )
+
 
 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 (file)
index 0000000..be49577
--- /dev/null
@@ -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.