Skip to content

Commit f4f8a71

Browse files
[3.13] gh-114053: Fix another edge case involving get_type_hints, PEP 695 and PEP 563 (GH-120272) (#121003)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 899dfba commit f4f8a71

File tree

4 files changed

+132
-11
lines changed

4 files changed

+132
-11
lines changed

Lib/test/test_typing.py

+62-4
Original file line numberDiff line numberDiff line change
@@ -4858,20 +4858,30 @@ def f(x: X): ...
48584858
{'x': list[list[ForwardRef('X')]]}
48594859
)
48604860

4861-
def test_pep695_generic_with_future_annotations(self):
4861+
def test_pep695_generic_class_with_future_annotations(self):
4862+
original_globals = dict(ann_module695.__dict__)
4863+
48624864
hints_for_A = get_type_hints(ann_module695.A)
48634865
A_type_params = ann_module695.A.__type_params__
48644866
self.assertIs(hints_for_A["x"], A_type_params[0])
48654867
self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]])
48664868
self.assertIs(hints_for_A["z"].__args__[0], A_type_params[2])
48674869

4870+
# should not have changed as a result of the get_type_hints() calls!
4871+
self.assertEqual(ann_module695.__dict__, original_globals)
4872+
4873+
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
48684874
hints_for_B = get_type_hints(ann_module695.B)
4869-
self.assertEqual(hints_for_B.keys(), {"x", "y", "z"})
4875+
self.assertEqual(hints_for_B, {"x": int, "y": str, "z": bytes})
4876+
4877+
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
4878+
hints_for_C = get_type_hints(ann_module695.C)
48704879
self.assertEqual(
4871-
set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
4872-
set()
4880+
set(hints_for_C.values()),
4881+
set(ann_module695.C.__type_params__)
48734882
)
48744883

4884+
def test_pep_695_generic_function_with_future_annotations(self):
48754885
hints_for_generic_function = get_type_hints(ann_module695.generic_function)
48764886
func_t_params = ann_module695.generic_function.__type_params__
48774887
self.assertEqual(
@@ -4882,6 +4892,54 @@ def test_pep695_generic_with_future_annotations(self):
48824892
self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
48834893
self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])
48844894

4895+
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
4896+
self.assertEqual(
4897+
set(get_type_hints(ann_module695.generic_function_2).values()),
4898+
set(ann_module695.generic_function_2.__type_params__)
4899+
)
4900+
4901+
def test_pep_695_generic_method_with_future_annotations(self):
4902+
hints_for_generic_method = get_type_hints(ann_module695.D.generic_method)
4903+
params = {
4904+
param.__name__: param
4905+
for param in ann_module695.D.generic_method.__type_params__
4906+
}
4907+
self.assertEqual(
4908+
hints_for_generic_method,
4909+
{"x": params["Foo"], "y": params["Bar"], "return": types.NoneType}
4910+
)
4911+
4912+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
4913+
self.assertEqual(
4914+
set(get_type_hints(ann_module695.D.generic_method_2).values()),
4915+
set(ann_module695.D.generic_method_2.__type_params__)
4916+
)
4917+
4918+
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
4919+
results = ann_module695.nested()
4920+
4921+
self.assertEqual(
4922+
set(results.hints_for_E.values()),
4923+
set(results.E.__type_params__)
4924+
)
4925+
self.assertEqual(
4926+
set(results.hints_for_E_meth.values()),
4927+
set(results.E.generic_method.__type_params__)
4928+
)
4929+
self.assertNotEqual(
4930+
set(results.hints_for_E_meth.values()),
4931+
set(results.E.__type_params__)
4932+
)
4933+
self.assertEqual(
4934+
set(results.hints_for_E_meth.values()).intersection(results.E.__type_params__),
4935+
set()
4936+
)
4937+
4938+
self.assertEqual(
4939+
set(results.hints_for_generic_func.values()),
4940+
set(results.generic_func.__type_params__)
4941+
)
4942+
48854943
def test_extended_generic_rules_subclassing(self):
48864944
class T1(Tuple[T, KT]): ...
48874945
class T2(Tuple[T, ...]): ...

Lib/test/typinganndata/ann_module695.py

+50
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,56 @@ class B[T, *Ts, **P]:
1717
z: P
1818

1919

20+
Eggs = int
21+
Spam = str
22+
23+
24+
class C[Eggs, **Spam]:
25+
x: Eggs
26+
y: Spam
27+
28+
2029
def generic_function[T, *Ts, **P](
2130
x: T, *y: *Ts, z: P.args, zz: P.kwargs
2231
) -> 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 typing import get_type_hints
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+
hints_for_E=get_type_hints(E),
69+
hints_for_E_meth=get_type_hints(E.generic_method),
70+
generic_func=generic_function,
71+
hints_for_generic_func=get_type_hints(generic_function)
72+
)

Lib/typing.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -1061,15 +1061,24 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard
10611061
globalns = getattr(
10621062
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
10631063
)
1064+
1065+
# type parameters require some special handling,
1066+
# as they exist in their own scope
1067+
# but `eval()` does not have a dedicated parameter for that scope.
1068+
# For classes, names in type parameter scopes should override
1069+
# names in the global scope (which here are called `localns`!),
1070+
# but should in turn be overridden by names in the class scope
1071+
# (which here are called `globalns`!)
10641072
if type_params:
1065-
# "Inject" type parameters into the local namespace
1066-
# (unless they are shadowed by assignments *in* the local namespace),
1067-
# as a way of emulating annotation scopes when calling `eval()`
1068-
locals_to_pass = {param.__name__: param for param in type_params} | localns
1069-
else:
1070-
locals_to_pass = localns
1073+
globalns, localns = dict(globalns), dict(localns)
1074+
for param in type_params:
1075+
param_name = param.__name__
1076+
if not self.__forward_is_class__ or param_name not in globalns:
1077+
globalns[param_name] = param
1078+
localns.pop(param_name, None)
1079+
10711080
type_ = _type_check(
1072-
eval(self.__forward_code__, globalns, locals_to_pass),
1081+
eval(self.__forward_code__, globalns, localns),
10731082
"Forward references must evaluate to types.",
10741083
is_argument=self.__forward_is_argument__,
10751084
allow_special_forms=self.__forward_is_class__,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix edge-case bug where :func:`typing.get_type_hints` would produce
2+
incorrect results if type parameters in a class scope were overridden by
3+
assignments in a class scope and ``from __future__ import annotations``
4+
semantics were enabled. Patch by Alex Waygood.

0 commit comments

Comments
 (0)