Skip to content

Commit a85241d

Browse files
[3.12] gh-114053: Fix another edge case involving get_type_hints, PEP 695 and PEP 563 (GH-120272) (#121004)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 89bc912 commit a85241d

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
@@ -4531,20 +4531,30 @@ def f(x: X): ...
45314531
{'x': list[list[ForwardRef('X')]]}
45324532
)
45334533

4534-
def test_pep695_generic_with_future_annotations(self):
4534+
def test_pep695_generic_class_with_future_annotations(self):
4535+
original_globals = dict(ann_module695.__dict__)
4536+
45354537
hints_for_A = get_type_hints(ann_module695.A)
45364538
A_type_params = ann_module695.A.__type_params__
45374539
self.assertIs(hints_for_A["x"], A_type_params[0])
45384540
self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]])
45394541
self.assertIs(hints_for_A["z"].__args__[0], A_type_params[2])
45404542

4543+
# should not have changed as a result of the get_type_hints() calls!
4544+
self.assertEqual(ann_module695.__dict__, original_globals)
4545+
4546+
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
45414547
hints_for_B = get_type_hints(ann_module695.B)
4542-
self.assertEqual(hints_for_B.keys(), {"x", "y", "z"})
4548+
self.assertEqual(hints_for_B, {"x": int, "y": str, "z": bytes})
4549+
4550+
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
4551+
hints_for_C = get_type_hints(ann_module695.C)
45434552
self.assertEqual(
4544-
set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
4545-
set()
4553+
set(hints_for_C.values()),
4554+
set(ann_module695.C.__type_params__)
45464555
)
45474556

4557+
def test_pep_695_generic_function_with_future_annotations(self):
45484558
hints_for_generic_function = get_type_hints(ann_module695.generic_function)
45494559
func_t_params = ann_module695.generic_function.__type_params__
45504560
self.assertEqual(
@@ -4555,6 +4565,54 @@ def test_pep695_generic_with_future_annotations(self):
45554565
self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
45564566
self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])
45574567

4568+
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
4569+
self.assertEqual(
4570+
set(get_type_hints(ann_module695.generic_function_2).values()),
4571+
set(ann_module695.generic_function_2.__type_params__)
4572+
)
4573+
4574+
def test_pep_695_generic_method_with_future_annotations(self):
4575+
hints_for_generic_method = get_type_hints(ann_module695.D.generic_method)
4576+
params = {
4577+
param.__name__: param
4578+
for param in ann_module695.D.generic_method.__type_params__
4579+
}
4580+
self.assertEqual(
4581+
hints_for_generic_method,
4582+
{"x": params["Foo"], "y": params["Bar"], "return": types.NoneType}
4583+
)
4584+
4585+
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
4586+
self.assertEqual(
4587+
set(get_type_hints(ann_module695.D.generic_method_2).values()),
4588+
set(ann_module695.D.generic_method_2.__type_params__)
4589+
)
4590+
4591+
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
4592+
results = ann_module695.nested()
4593+
4594+
self.assertEqual(
4595+
set(results.hints_for_E.values()),
4596+
set(results.E.__type_params__)
4597+
)
4598+
self.assertEqual(
4599+
set(results.hints_for_E_meth.values()),
4600+
set(results.E.generic_method.__type_params__)
4601+
)
4602+
self.assertNotEqual(
4603+
set(results.hints_for_E_meth.values()),
4604+
set(results.E.__type_params__)
4605+
)
4606+
self.assertEqual(
4607+
set(results.hints_for_E_meth.values()).intersection(results.E.__type_params__),
4608+
set()
4609+
)
4610+
4611+
self.assertEqual(
4612+
set(results.hints_for_generic_func.values()),
4613+
set(results.generic_func.__type_params__)
4614+
)
4615+
45584616
def test_extended_generic_rules_subclassing(self):
45594617
class T1(Tuple[T, KT]): ...
45604618
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
@@ -927,15 +927,24 @@ def _evaluate(self, globalns, localns, type_params=None, *, recursive_guard):
927927
globalns = getattr(
928928
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
929929
)
930+
931+
# type parameters require some special handling,
932+
# as they exist in their own scope
933+
# but `eval()` does not have a dedicated parameter for that scope.
934+
# For classes, names in type parameter scopes should override
935+
# names in the global scope (which here are called `localns`!),
936+
# but should in turn be overridden by names in the class scope
937+
# (which here are called `globalns`!)
930938
if type_params:
931-
# "Inject" type parameters into the local namespace
932-
# (unless they are shadowed by assignments *in* the local namespace),
933-
# as a way of emulating annotation scopes when calling `eval()`
934-
locals_to_pass = {param.__name__: param for param in type_params} | localns
935-
else:
936-
locals_to_pass = localns
939+
globalns, localns = dict(globalns), dict(localns)
940+
for param in type_params:
941+
param_name = param.__name__
942+
if not self.__forward_is_class__ or param_name not in globalns:
943+
globalns[param_name] = param
944+
localns.pop(param_name, None)
945+
937946
type_ = _type_check(
938-
eval(self.__forward_code__, globalns, locals_to_pass),
947+
eval(self.__forward_code__, globalns, localns),
939948
"Forward references must evaluate to types.",
940949
is_argument=self.__forward_is_argument__,
941950
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)