https://github.com/python/cpython/commit/42351c3b9a357ec67135b30ed41f59e6f306ac52
commit: 42351c3b9a357ec67135b30ed41f59e6f306ac52
branch: main
author: Alex Waygood <[email protected]>
committer: AlexWaygood <[email protected]>
date: 2024-06-13T21:16:40Z
summary:
gh-114053: Fix bad interaction of PEP 695, PEP 563 and
`inspect.get_annotations` (#120270)
files:
A Lib/test/test_inspect/inspect_stringized_annotations_pep695.py
A Misc/NEWS.d/next/Library/2024-06-08-15-15-29.gh-issue-114053.WQLAFG.rst
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 5570a43ebfea19..11544b8d0d4932 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -274,7 +274,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..723822f8eaa92d
--- /dev/null
+++ b/Lib/test/test_inspect/inspect_stringized_annotations_pep695.py
@@ -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)
+ )
diff --git a/Lib/test/test_inspect/test_inspect.py
b/Lib/test/test_inspect/test_inspect.py
index 0a4fa9343f15e0..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
@@ -47,6 +48,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 +1694,107 @@ 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_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
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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]