Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ece1201

Browse files
authored
Backport bugfixes made to how inspect.get_annotations() deals with PEP-695 (#428)
1 parent 2d33f1f commit ece1201

File tree

3 files changed

+239
-3
lines changed

3 files changed

+239
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
# Release 4.12.2 (June 7, 2024)
1+
# Unreleased
22

33
- Add `typing_extensions.get_annotations`, a backport of
44
`inspect.get_annotations` that adds features specified
5-
by PEP 649. Patch by Jelle Zijlstra.
5+
by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
6+
7+
# Release 4.12.2 (June 7, 2024)
8+
69
- Fix regression in v4.12.0 where specialization of certain
710
generics with an overridden `__eq__` method would raise errors.
811
Patch by Jelle Zijlstra.

src/test_typing_extensions.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0)
112112

113113
# 3.12 changes the representation of Unpack[] (PEP 692)
114+
# and adds PEP 695 to CPython's grammar
114115
TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0)
115116

116117
# 3.13 drops support for the keyword argument syntax of TypedDict
@@ -268,6 +269,7 @@ class UnannotatedClass:
268269
269270
def unannotated_function(a, b, c): pass
270271
"""
272+
271273
STRINGIZED_ANNOTATIONS = """
272274
from __future__ import annotations
273275
@@ -304,13 +306,110 @@ class MyClassWithLocalAnnotations:
304306
mytype = int
305307
x: mytype
306308
"""
309+
307310
STRINGIZED_ANNOTATIONS_2 = """
308311
from __future__ import annotations
309312
310313
311314
def foo(a, b, c): pass
312315
"""
313316

317+
if TYPING_3_12_0:
318+
STRINGIZED_ANNOTATIONS_PEP_695 = textwrap.dedent(
319+
"""
320+
from __future__ import annotations
321+
from typing import Callable, Unpack
322+
323+
324+
class A[T, *Ts, **P]:
325+
x: T
326+
y: tuple[*Ts]
327+
z: Callable[P, str]
328+
329+
330+
class B[T, *Ts, **P]:
331+
T = int
332+
Ts = str
333+
P = bytes
334+
x: T
335+
y: Ts
336+
z: P
337+
338+
339+
Eggs = int
340+
Spam = str
341+
342+
343+
class C[Eggs, **Spam]:
344+
x: Eggs
345+
y: Spam
346+
347+
348+
def generic_function[T, *Ts, **P](
349+
x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs
350+
) -> None: ...
351+
352+
353+
def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass
354+
355+
356+
class D:
357+
Foo = int
358+
Bar = str
359+
360+
def generic_method[Foo, **Bar](
361+
self, x: Foo, y: Bar
362+
) -> None: ...
363+
364+
def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass
365+
366+
367+
# Eggs is `int` in globals, a TypeVar in type_params, and `str` in locals:
368+
class E[Eggs]:
369+
Eggs = str
370+
x: Eggs
371+
372+
373+
374+
def nested():
375+
from types import SimpleNamespace
376+
from typing_extensions import get_annotations
377+
378+
Eggs = bytes
379+
Spam = memoryview
380+
381+
382+
class F[Eggs, **Spam]:
383+
x: Eggs
384+
y: Spam
385+
386+
def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass
387+
388+
389+
def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass
390+
391+
392+
# Eggs is `int` in globals, `bytes` in the function scope,
393+
# a TypeVar in the type_params, and `str` in locals:
394+
class G[Eggs]:
395+
Eggs = str
396+
x: Eggs
397+
398+
399+
return SimpleNamespace(
400+
F=F,
401+
F_annotations=get_annotations(F, eval_str=True),
402+
F_meth_annotations=get_annotations(F.generic_method, eval_str=True),
403+
G_annotations=get_annotations(G, eval_str=True),
404+
generic_func=generic_function,
405+
generic_func_annotations=get_annotations(generic_function, eval_str=True)
406+
)
407+
"""
408+
)
409+
else:
410+
STRINGIZED_ANNOTATIONS_PEP_695 = None
411+
412+
314413
class BaseTestCase(TestCase):
315414
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
316415
if not issubclass(cls, class_or_tuple):
@@ -7489,6 +7588,134 @@ def f(x: int):
74897588
self.assertEqual(get_annotations(f), {"x": str})
74907589

74917590

7591+
@skipIf(STRINGIZED_ANNOTATIONS_PEP_695 is None, "PEP 695 has yet to be")
7592+
class TestGetAnnotationsWithPEP695(BaseTestCase):
7593+
@classmethod
7594+
def setUpClass(cls):
7595+
with tempfile.TemporaryDirectory() as tempdir:
7596+
sys.path.append(tempdir)
7597+
Path(tempdir, "inspect_stringized_annotations_pep_695.py").write_text(STRINGIZED_ANNOTATIONS_PEP_695)
7598+
cls.inspect_stringized_annotations_pep_695 = importlib.import_module(
7599+
"inspect_stringized_annotations_pep_695"
7600+
)
7601+
sys.path.pop()
7602+
7603+
@classmethod
7604+
def tearDownClass(cls):
7605+
del cls.inspect_stringized_annotations_pep_695
7606+
del sys.modules["inspect_stringized_annotations_pep_695"]
7607+
7608+
def test_pep695_generic_class_with_future_annotations(self):
7609+
ann_module695 = self.inspect_stringized_annotations_pep_695
7610+
A_annotations = get_annotations(ann_module695.A, eval_str=True)
7611+
A_type_params = ann_module695.A.__type_params__
7612+
self.assertIs(A_annotations["x"], A_type_params[0])
7613+
self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]])
7614+
self.assertIs(A_annotations["z"].__args__[0], A_type_params[2])
7615+
7616+
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
7617+
B_annotations = get_annotations(
7618+
self.inspect_stringized_annotations_pep_695.B, eval_str=True
7619+
)
7620+
self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes})
7621+
7622+
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
7623+
ann_module695 = self.inspect_stringized_annotations_pep_695
7624+
C_annotations = get_annotations(ann_module695.C, eval_str=True)
7625+
self.assertEqual(
7626+
set(C_annotations.values()),
7627+
set(ann_module695.C.__type_params__)
7628+
)
7629+
7630+
def test_pep_695_generic_function_with_future_annotations(self):
7631+
ann_module695 = self.inspect_stringized_annotations_pep_695
7632+
generic_func_annotations = get_annotations(
7633+
ann_module695.generic_function, eval_str=True
7634+
)
7635+
func_t_params = ann_module695.generic_function.__type_params__
7636+
self.assertEqual(
7637+
generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"}
7638+
)
7639+
self.assertIs(generic_func_annotations["x"], func_t_params[0])
7640+
self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]])
7641+
self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2])
7642+
self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2])
7643+
7644+
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
7645+
self.assertEqual(
7646+
set(
7647+
get_annotations(
7648+
self.inspect_stringized_annotations_pep_695.generic_function_2,
7649+
eval_str=True
7650+
).values()
7651+
),
7652+
set(
7653+
self.inspect_stringized_annotations_pep_695.generic_function_2.__type_params__
7654+
)
7655+
)
7656+
7657+
def test_pep_695_generic_method_with_future_annotations(self):
7658+
ann_module695 = self.inspect_stringized_annotations_pep_695
7659+
generic_method_annotations = get_annotations(
7660+
ann_module695.D.generic_method, eval_str=True
7661+
)
7662+
params = {
7663+
param.__name__: param
7664+
for param in ann_module695.D.generic_method.__type_params__
7665+
}
7666+
self.assertEqual(
7667+
generic_method_annotations,
7668+
{"x": params["Foo"], "y": params["Bar"], "return": None}
7669+
)
7670+
7671+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
7672+
self.assertEqual(
7673+
set(
7674+
get_annotations(
7675+
self.inspect_stringized_annotations_pep_695.D.generic_method_2,
7676+
eval_str=True
7677+
).values()
7678+
),
7679+
set(
7680+
self.inspect_stringized_annotations_pep_695.D.generic_method_2.__type_params__
7681+
)
7682+
)
7683+
7684+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self):
7685+
self.assertEqual(
7686+
get_annotations(
7687+
self.inspect_stringized_annotations_pep_695.E, eval_str=True
7688+
),
7689+
{"x": str},
7690+
)
7691+
7692+
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
7693+
results = self.inspect_stringized_annotations_pep_695.nested()
7694+
7695+
self.assertEqual(
7696+
set(results.F_annotations.values()),
7697+
set(results.F.__type_params__)
7698+
)
7699+
self.assertEqual(
7700+
set(results.F_meth_annotations.values()),
7701+
set(results.F.generic_method.__type_params__)
7702+
)
7703+
self.assertNotEqual(
7704+
set(results.F_meth_annotations.values()),
7705+
set(results.F.__type_params__)
7706+
)
7707+
self.assertEqual(
7708+
set(results.F_meth_annotations.values()).intersection(results.F.__type_params__),
7709+
set()
7710+
)
7711+
7712+
self.assertEqual(results.G_annotations, {"x": str})
7713+
7714+
self.assertEqual(
7715+
set(results.generic_func_annotations.values()),
7716+
set(results.generic_func.__type_params__)
7717+
)
7718+
74927719

74937720
if __name__ == '__main__':
74947721
main()

src/typing_extensions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3734,7 +3734,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False,
37343734
if globals is None:
37353735
globals = obj_globals
37363736
if locals is None:
3737-
locals = obj_locals
3737+
locals = obj_locals or {}
3738+
3739+
# "Inject" type parameters into the local namespace
3740+
# (unless they are shadowed by assignments *in* the local namespace),
3741+
# as a way of emulating annotation scopes when calling `eval()`
3742+
if type_params := getattr(obj, "__type_params__", ()):
3743+
locals = {param.__name__: param for param in type_params} | locals
37383744

37393745
return_value = {key:
37403746
value if not isinstance(value, str) else eval(value, globals, locals)

0 commit comments

Comments
 (0)