Skip to content

Commit

Permalink
gh-114053: Fix bad interaction of PEP 695, PEP 563 and `inspect.get_a…
Browse files Browse the repository at this point in the history
…nnotations` (#120270)
  • Loading branch information
AlexWaygood authored Jun 13, 2024
1 parent d88a1f2 commit 42351c3
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
8 changes: 7 additions & 1 deletion Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_inspect/inspect_stringized_annotations_pep695.py
Original file line number Diff line number Diff line change
@@ -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)
)
103 changes: 103 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import types
import tempfile
import textwrap
from typing import Unpack
import unicodedata
import unittest
import unittest.mock
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 42351c3

Please sign in to comment.