Skip to content

Commit 1834453

Browse files
[3.12] gh-114053: Fix bad interaction of PEP 695, PEP 563 and inspect.get_annotations (GH-120270) (#120475)
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 c9fc099 commit 1834453

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
@@ -17,6 +17,7 @@
1717
import sys
1818
import types
1919
import textwrap
20+
from typing import Unpack
2021
import unicodedata
2122
import unittest
2223
import unittest.mock
@@ -40,6 +41,7 @@
4041
from test.test_inspect import inspect_stock_annotations
4142
from test.test_inspect import inspect_stringized_annotations
4243
from test.test_inspect import inspect_stringized_annotations_2
44+
from test.test_inspect import inspect_stringized_annotations_pep695
4345

4446

4547
# Functions tested in this suite:
@@ -1505,6 +1507,107 @@ def wrapper(a, b):
15051507
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'})
15061508
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
15071509

1510+
def test_pep695_generic_class_with_future_annotations(self):
1511+
ann_module695 = inspect_stringized_annotations_pep695
1512+
A_annotations = inspect.get_annotations(ann_module695.A, eval_str=True)
1513+
A_type_params = ann_module695.A.__type_params__
1514+
self.assertIs(A_annotations["x"], A_type_params[0])
1515+
self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]])
1516+
self.assertIs(A_annotations["z"].__args__[0], A_type_params[2])
1517+
1518+
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
1519+
B_annotations = inspect.get_annotations(
1520+
inspect_stringized_annotations_pep695.B, eval_str=True
1521+
)
1522+
self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes})
1523+
1524+
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
1525+
ann_module695 = inspect_stringized_annotations_pep695
1526+
C_annotations = inspect.get_annotations(ann_module695.C, eval_str=True)
1527+
self.assertEqual(
1528+
set(C_annotations.values()),
1529+
set(ann_module695.C.__type_params__)
1530+
)
1531+
1532+
def test_pep_695_generic_function_with_future_annotations(self):
1533+
ann_module695 = inspect_stringized_annotations_pep695
1534+
generic_func_annotations = inspect.get_annotations(
1535+
ann_module695.generic_function, eval_str=True
1536+
)
1537+
func_t_params = ann_module695.generic_function.__type_params__
1538+
self.assertEqual(
1539+
generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"}
1540+
)
1541+
self.assertIs(generic_func_annotations["x"], func_t_params[0])
1542+
self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]])
1543+
self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2])
1544+
self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2])
1545+
1546+
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
1547+
self.assertEqual(
1548+
set(
1549+
inspect.get_annotations(
1550+
inspect_stringized_annotations_pep695.generic_function_2,
1551+
eval_str=True
1552+
).values()
1553+
),
1554+
set(
1555+
inspect_stringized_annotations_pep695.generic_function_2.__type_params__
1556+
)
1557+
)
1558+
1559+
def test_pep_695_generic_method_with_future_annotations(self):
1560+
ann_module695 = inspect_stringized_annotations_pep695
1561+
generic_method_annotations = inspect.get_annotations(
1562+
ann_module695.D.generic_method, eval_str=True
1563+
)
1564+
params = {
1565+
param.__name__: param
1566+
for param in ann_module695.D.generic_method.__type_params__
1567+
}
1568+
self.assertEqual(
1569+
generic_method_annotations,
1570+
{"x": params["Foo"], "y": params["Bar"], "return": None}
1571+
)
1572+
1573+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
1574+
self.assertEqual(
1575+
set(
1576+
inspect.get_annotations(
1577+
inspect_stringized_annotations_pep695.D.generic_method_2,
1578+
eval_str=True
1579+
).values()
1580+
),
1581+
set(
1582+
inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__
1583+
)
1584+
)
1585+
1586+
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
1587+
results = inspect_stringized_annotations_pep695.nested()
1588+
1589+
self.assertEqual(
1590+
set(results.E_annotations.values()),
1591+
set(results.E.__type_params__)
1592+
)
1593+
self.assertEqual(
1594+
set(results.E_meth_annotations.values()),
1595+
set(results.E.generic_method.__type_params__)
1596+
)
1597+
self.assertNotEqual(
1598+
set(results.E_meth_annotations.values()),
1599+
set(results.E.__type_params__)
1600+
)
1601+
self.assertEqual(
1602+
set(results.E_meth_annotations.values()).intersection(results.E.__type_params__),
1603+
set()
1604+
)
1605+
1606+
self.assertEqual(
1607+
set(results.generic_func_annotations.values()),
1608+
set(results.generic_func.__type_params__)
1609+
)
1610+
15081611

15091612
class TestFormatAnnotation(unittest.TestCase):
15101613
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)