Skip to content

Commit d4174fa

Browse files
[3.13] gh-114053: Fix bad interaction of PEP 695, PEP 563 and inspect.get_annotations (GH-120270) (#120474)
gh-114053: Fix bad interaction of PEP 695, PEP 563 and `inspect.get_annotations` (GH-120270) (cherry picked from commit 42351c3) Co-authored-by: Alex Waygood <[email protected]>
1 parent d5ad3b7 commit d4174fa

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

Lib/inspect.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
280280
if globals is None:
281281
globals = obj_globals
282282
if locals is None:
283-
locals = obj_locals
283+
locals = obj_locals or {}
284+
285+
# "Inject" type parameters into the local namespace
286+
# (unless they are shadowed by assignments *in* the local namespace),
287+
# as a way of emulating annotation scopes when calling `eval()`
288+
if type_params := getattr(obj, "__type_params__", ()):
289+
locals = {param.__name__: param for param in type_params} | locals
284290

285291
return_value = {key:
286292
value if not isinstance(value, str) else eval(value, globals, locals)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
from typing import Callable, Unpack
3+
4+
5+
class A[T, *Ts, **P]:
6+
x: T
7+
y: tuple[*Ts]
8+
z: Callable[P, str]
9+
10+
11+
class B[T, *Ts, **P]:
12+
T = int
13+
Ts = str
14+
P = bytes
15+
x: T
16+
y: Ts
17+
z: P
18+
19+
20+
Eggs = int
21+
Spam = str
22+
23+
24+
class C[Eggs, **Spam]:
25+
x: Eggs
26+
y: Spam
27+
28+
29+
def generic_function[T, *Ts, **P](
30+
x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs
31+
) -> None: ...
32+
33+
34+
def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass
35+
36+
37+
class D:
38+
Foo = int
39+
Bar = str
40+
41+
def generic_method[Foo, **Bar](
42+
self, x: Foo, y: Bar
43+
) -> None: ...
44+
45+
def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass
46+
47+
48+
def nested():
49+
from types import SimpleNamespace
50+
from inspect import get_annotations
51+
52+
Eggs = bytes
53+
Spam = memoryview
54+
55+
56+
class E[Eggs, **Spam]:
57+
x: Eggs
58+
y: Spam
59+
60+
def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass
61+
62+
63+
def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass
64+
65+
66+
return SimpleNamespace(
67+
E=E,
68+
E_annotations=get_annotations(E, eval_str=True),
69+
E_meth_annotations=get_annotations(E.generic_method, eval_str=True),
70+
generic_func=generic_function,
71+
generic_func_annotations=get_annotations(generic_function, eval_str=True)
72+
)

Lib/test/test_inspect/test_inspect.py

+103
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import types
2323
import tempfile
2424
import textwrap
25+
from typing import Unpack
2526
import unicodedata
2627
import unittest
2728
import unittest.mock
@@ -47,6 +48,7 @@
4748
from test.test_inspect import inspect_stock_annotations
4849
from test.test_inspect import inspect_stringized_annotations
4950
from test.test_inspect import inspect_stringized_annotations_2
51+
from test.test_inspect import inspect_stringized_annotations_pep695
5052

5153

5254
# Functions tested in this suite:
@@ -1692,6 +1694,107 @@ def wrapper(a, b):
16921694
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'})
16931695
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
16941696

1697+
def test_pep695_generic_class_with_future_annotations(self):
1698+
ann_module695 = inspect_stringized_annotations_pep695
1699+
A_annotations = inspect.get_annotations(ann_module695.A, eval_str=True)
1700+
A_type_params = ann_module695.A.__type_params__
1701+
self.assertIs(A_annotations["x"], A_type_params[0])
1702+
self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]])
1703+
self.assertIs(A_annotations["z"].__args__[0], A_type_params[2])
1704+
1705+
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
1706+
B_annotations = inspect.get_annotations(
1707+
inspect_stringized_annotations_pep695.B, eval_str=True
1708+
)
1709+
self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes})
1710+
1711+
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
1712+
ann_module695 = inspect_stringized_annotations_pep695
1713+
C_annotations = inspect.get_annotations(ann_module695.C, eval_str=True)
1714+
self.assertEqual(
1715+
set(C_annotations.values()),
1716+
set(ann_module695.C.__type_params__)
1717+
)
1718+
1719+
def test_pep_695_generic_function_with_future_annotations(self):
1720+
ann_module695 = inspect_stringized_annotations_pep695
1721+
generic_func_annotations = inspect.get_annotations(
1722+
ann_module695.generic_function, eval_str=True
1723+
)
1724+
func_t_params = ann_module695.generic_function.__type_params__
1725+
self.assertEqual(
1726+
generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"}
1727+
)
1728+
self.assertIs(generic_func_annotations["x"], func_t_params[0])
1729+
self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]])
1730+
self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2])
1731+
self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2])
1732+
1733+
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
1734+
self.assertEqual(
1735+
set(
1736+
inspect.get_annotations(
1737+
inspect_stringized_annotations_pep695.generic_function_2,
1738+
eval_str=True
1739+
).values()
1740+
),
1741+
set(
1742+
inspect_stringized_annotations_pep695.generic_function_2.__type_params__
1743+
)
1744+
)
1745+
1746+
def test_pep_695_generic_method_with_future_annotations(self):
1747+
ann_module695 = inspect_stringized_annotations_pep695
1748+
generic_method_annotations = inspect.get_annotations(
1749+
ann_module695.D.generic_method, eval_str=True
1750+
)
1751+
params = {
1752+
param.__name__: param
1753+
for param in ann_module695.D.generic_method.__type_params__
1754+
}
1755+
self.assertEqual(
1756+
generic_method_annotations,
1757+
{"x": params["Foo"], "y": params["Bar"], "return": None}
1758+
)
1759+
1760+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
1761+
self.assertEqual(
1762+
set(
1763+
inspect.get_annotations(
1764+
inspect_stringized_annotations_pep695.D.generic_method_2,
1765+
eval_str=True
1766+
).values()
1767+
),
1768+
set(
1769+
inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__
1770+
)
1771+
)
1772+
1773+
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
1774+
results = inspect_stringized_annotations_pep695.nested()
1775+
1776+
self.assertEqual(
1777+
set(results.E_annotations.values()),
1778+
set(results.E.__type_params__)
1779+
)
1780+
self.assertEqual(
1781+
set(results.E_meth_annotations.values()),
1782+
set(results.E.generic_method.__type_params__)
1783+
)
1784+
self.assertNotEqual(
1785+
set(results.E_meth_annotations.values()),
1786+
set(results.E.__type_params__)
1787+
)
1788+
self.assertEqual(
1789+
set(results.E_meth_annotations.values()).intersection(results.E.__type_params__),
1790+
set()
1791+
)
1792+
1793+
self.assertEqual(
1794+
set(results.generic_func_annotations.values()),
1795+
set(results.generic_func.__type_params__)
1796+
)
1797+
16951798

16961799
class TestFormatAnnotation(unittest.TestCase):
16971800
def test_typing_replacement(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix erroneous :exc:`NameError` when calling :func:`inspect.get_annotations`
2+
with ``eval_str=True``` on a class that made use of :pep:`695` type
3+
parameters in a module that had ``from __future__ import annotations`` at
4+
the top of the file. Patch by Alex Waygood.

0 commit comments

Comments
 (0)