Skip to content
23 changes: 20 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4859,17 +4859,21 @@ def f(x: X): ...
)

def test_pep695_generic_with_future_annotations(self):
original_globals = dict(ann_module695.__dict__)

hints_for_A = get_type_hints(ann_module695.A)
A_type_params = ann_module695.A.__type_params__
self.assertIs(hints_for_A["x"], A_type_params[0])
self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]])
self.assertIs(hints_for_A["z"].__args__[0], A_type_params[2])

hints_for_B = get_type_hints(ann_module695.B)
self.assertEqual(hints_for_B.keys(), {"x", "y", "z"})
self.assertEqual(hints_for_B, {"x": int, "y": str, "z": bytes})

hints_for_C = get_type_hints(ann_module695.C)
self.assertEqual(
set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
set()
set(hints_for_C.values()),
set(ann_module695.C.__type_params__)
)

hints_for_generic_function = get_type_hints(ann_module695.generic_function)
Expand All @@ -4882,6 +4886,19 @@ def test_pep695_generic_with_future_annotations(self):
self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])

hints_for_generic_method = get_type_hints(ann_module695.D.generic_method)
params = {
param.__name__: param
for param in ann_module695.D.generic_method.__type_params__
}
self.assertEqual(
hints_for_generic_method,
{"x": params["Foo"], "y": params["Bar"], "return": types.NoneType}
)

# should not have changed as a result of the get_type_hints() calls!
self.assertEqual(ann_module695.__dict__, original_globals)

def test_extended_generic_rules_subclassing(self):
class T1(Tuple[T, KT]): ...
class T2(Tuple[T, ...]): ...
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/typinganndata/ann_module695.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ class B[T, *Ts, **P]:
z: P


Eggs = int
Spam = str


class C[Eggs, **Spam]:
x: Eggs
y: Spam


def generic_function[T, *Ts, **P](
x: T, *y: *Ts, z: P.args, zz: P.kwargs
) -> None: ...


class D:
Foo = int
Bar = str

def generic_method[Foo, **Bar](
self, x: Foo, y: Bar
) -> None: ...
26 changes: 19 additions & 7 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,15 +1060,27 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard
globalns = getattr(
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
)

# type parameters require some special handling,
# as they exist in their own scope
# but `eval()` does not have a dedicated parameter for that scope.
# For classes, names in type parameter scopes should override
# names in the global scope (which here are called `localns`!),
# but should in turn be overridden by names in the class scope
# (which here are called `globalns`!)
if type_params:
# "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()`
locals_to_pass = {param.__name__: param for param in type_params} | localns
else:
locals_to_pass = localns
if self.__forward_is_class__:
globalns, localns = dict(globalns), dict(localns)
for param in type_params:
param_name = param.__name__
if param_name not in globalns:
globalns[param_name] = param
localns.pop(param_name, None)
else:
localns = {param.__name__: param for param in type_params} | localns

type_ = _type_check(
eval(self.__forward_code__, globalns, locals_to_pass),
eval(self.__forward_code__, globalns, localns),
"Forward references must evaluate to types.",
is_argument=self.__forward_is_argument__,
allow_special_forms=self.__forward_is_class__,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix edge-case bug where :func:`typing.get_type_hints` would produce
incorrect results if type parameters in a class scope were overridden by
assignments in a class scope and ``from __future__ import annotations``
semantics were enabled. Patch by Alex Waygood.