Backport bugfixes made to how `inspect.get_annotations()` deals with PEP-695 (#428)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89300be..c022682 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
-# Release 4.12.2 (June 7, 2024)
+# Unreleased
- Add `typing_extensions.get_annotations`, a backport of
`inspect.get_annotations` that adds features specified
- by PEP 649. Patch by Jelle Zijlstra.
+ by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
+
+# Release 4.12.2 (June 7, 2024)
+
- Fix regression in v4.12.0 where specialization of certain
generics with an overridden `__eq__` method would raise errors.
Patch by Jelle Zijlstra.
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 331a643..362845f 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -111,6 +111,7 @@
TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0)
# 3.12 changes the representation of Unpack[] (PEP 692)
+# and adds PEP 695 to CPython's grammar
TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0)
# 3.13 drops support for the keyword argument syntax of TypedDict
@@ -268,6 +269,7 @@
def unannotated_function(a, b, c): pass
"""
+
STRINGIZED_ANNOTATIONS = """
from __future__ import annotations
@@ -304,6 +306,7 @@
mytype = int
x: mytype
"""
+
STRINGIZED_ANNOTATIONS_2 = """
from __future__ import annotations
@@ -311,6 +314,102 @@
def foo(a, b, c): pass
"""
+if TYPING_3_12_0:
+ STRINGIZED_ANNOTATIONS_PEP_695 = textwrap.dedent(
+ """
+ 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
+
+
+ # Eggs is `int` in globals, a TypeVar in type_params, and `str` in locals:
+ class E[Eggs]:
+ Eggs = str
+ x: Eggs
+
+
+
+ def nested():
+ from types import SimpleNamespace
+ from typing_extensions import get_annotations
+
+ Eggs = bytes
+ Spam = memoryview
+
+
+ class F[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
+
+
+ # Eggs is `int` in globals, `bytes` in the function scope,
+ # a TypeVar in the type_params, and `str` in locals:
+ class G[Eggs]:
+ Eggs = str
+ x: Eggs
+
+
+ return SimpleNamespace(
+ F=F,
+ F_annotations=get_annotations(F, eval_str=True),
+ F_meth_annotations=get_annotations(F.generic_method, eval_str=True),
+ G_annotations=get_annotations(G, eval_str=True),
+ generic_func=generic_function,
+ generic_func_annotations=get_annotations(generic_function, eval_str=True)
+ )
+ """
+ )
+else:
+ STRINGIZED_ANNOTATIONS_PEP_695 = None
+
+
class BaseTestCase(TestCase):
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
if not issubclass(cls, class_or_tuple):
@@ -7489,6 +7588,134 @@
self.assertEqual(get_annotations(f), {"x": str})
+@skipIf(STRINGIZED_ANNOTATIONS_PEP_695 is None, "PEP 695 has yet to be")
+class TestGetAnnotationsWithPEP695(BaseTestCase):
+ @classmethod
+ def setUpClass(cls):
+ with tempfile.TemporaryDirectory() as tempdir:
+ sys.path.append(tempdir)
+ Path(tempdir, "inspect_stringized_annotations_pep_695.py").write_text(STRINGIZED_ANNOTATIONS_PEP_695)
+ cls.inspect_stringized_annotations_pep_695 = importlib.import_module(
+ "inspect_stringized_annotations_pep_695"
+ )
+ sys.path.pop()
+
+ @classmethod
+ def tearDownClass(cls):
+ del cls.inspect_stringized_annotations_pep_695
+ del sys.modules["inspect_stringized_annotations_pep_695"]
+
+ def test_pep695_generic_class_with_future_annotations(self):
+ ann_module695 = self.inspect_stringized_annotations_pep_695
+ A_annotations = 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 = get_annotations(
+ self.inspect_stringized_annotations_pep_695.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 = self.inspect_stringized_annotations_pep_695
+ C_annotations = 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 = self.inspect_stringized_annotations_pep_695
+ generic_func_annotations = 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(
+ get_annotations(
+ self.inspect_stringized_annotations_pep_695.generic_function_2,
+ eval_str=True
+ ).values()
+ ),
+ set(
+ self.inspect_stringized_annotations_pep_695.generic_function_2.__type_params__
+ )
+ )
+
+ def test_pep_695_generic_method_with_future_annotations(self):
+ ann_module695 = self.inspect_stringized_annotations_pep_695
+ generic_method_annotations = 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(
+ get_annotations(
+ self.inspect_stringized_annotations_pep_695.D.generic_method_2,
+ eval_str=True
+ ).values()
+ ),
+ set(
+ self.inspect_stringized_annotations_pep_695.D.generic_method_2.__type_params__
+ )
+ )
+
+ def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self):
+ self.assertEqual(
+ get_annotations(
+ self.inspect_stringized_annotations_pep_695.E, eval_str=True
+ ),
+ {"x": str},
+ )
+
+ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
+ results = self.inspect_stringized_annotations_pep_695.nested()
+
+ self.assertEqual(
+ set(results.F_annotations.values()),
+ set(results.F.__type_params__)
+ )
+ self.assertEqual(
+ set(results.F_meth_annotations.values()),
+ set(results.F.generic_method.__type_params__)
+ )
+ self.assertNotEqual(
+ set(results.F_meth_annotations.values()),
+ set(results.F.__type_params__)
+ )
+ self.assertEqual(
+ set(results.F_meth_annotations.values()).intersection(results.F.__type_params__),
+ set()
+ )
+
+ self.assertEqual(results.G_annotations, {"x": str})
+
+ self.assertEqual(
+ set(results.generic_func_annotations.values()),
+ set(results.generic_func.__type_params__)
+ )
+
if __name__ == '__main__':
main()
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index 342a749..d5d0a11 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -3734,7 +3734,13 @@
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)