Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-114053: Fix another edge case involving get_type_hints, PEP 695 and PEP 563 #120272

Merged
merged 10 commits into from
Jun 25, 2024
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:
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
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.
Loading