mirror of https://github.com/python/cpython
[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 42351c3b9a
)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
d5ad3b7fc8
commit
d4174fa7ca
|
@ -280,7 +280,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
|
||||||
if globals is None:
|
if globals is None:
|
||||||
globals = obj_globals
|
globals = obj_globals
|
||||||
if locals is None:
|
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:
|
return_value = {key:
|
||||||
value if not isinstance(value, str) else eval(value, globals, locals)
|
value if not isinstance(value, str) else eval(value, globals, locals)
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
|
@ -22,6 +22,7 @@ import time
|
||||||
import types
|
import types
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from typing import Unpack
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock
|
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_stock_annotations
|
||||||
from test.test_inspect import inspect_stringized_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_2
|
||||||
|
from test.test_inspect import inspect_stringized_annotations_pep695
|
||||||
|
|
||||||
|
|
||||||
# Functions tested in this suite:
|
# 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), {'x': 'mytype'})
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
|
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):
|
class TestFormatAnnotation(unittest.TestCase):
|
||||||
def test_typing_replacement(self):
|
def test_typing_replacement(self):
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue