Skip to content

Commit 5430f61

Browse files
[3.12] gh-114053: Fix bad interaction of PEP-695, PEP-563 and get_type_hints (#118009) (#118104)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 6584ad9 commit 5430f61

File tree

5 files changed

+81
-10
lines changed

5 files changed

+81
-10
lines changed

Doc/library/typing.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -2887,7 +2887,9 @@ Introspection helpers
28872887

28882888
This is often the same as ``obj.__annotations__``. In addition,
28892889
forward references encoded as string literals are handled by evaluating
2890-
them in ``globals`` and ``locals`` namespaces. For a class ``C``, return
2890+
them in ``globals``, ``locals`` and (where applicable)
2891+
:ref:`type parameter <type-params>` namespaces.
2892+
For a class ``C``, return
28912893
a dictionary constructed by merging all the ``__annotations__`` along
28922894
``C.__mro__`` in reverse order.
28932895

Lib/test/test_typing.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import types
4848

4949
from test.support import captured_stderr, cpython_only
50-
from test.typinganndata import mod_generics_cache, _typed_dict_helper
50+
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper
5151

5252

5353
CANNOT_SUBCLASS_TYPE = 'Cannot subclass special typing classes'
@@ -4499,6 +4499,30 @@ def f(x: X): ...
44994499
{'x': list[list[ForwardRef('X')]]}
45004500
)
45014501

4502+
def test_pep695_generic_with_future_annotations(self):
4503+
hints_for_A = get_type_hints(ann_module695.A)
4504+
A_type_params = ann_module695.A.__type_params__
4505+
self.assertIs(hints_for_A["x"], A_type_params[0])
4506+
self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]])
4507+
self.assertIs(hints_for_A["z"].__args__[0], A_type_params[2])
4508+
4509+
hints_for_B = get_type_hints(ann_module695.B)
4510+
self.assertEqual(hints_for_B.keys(), {"x", "y", "z"})
4511+
self.assertEqual(
4512+
set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
4513+
set()
4514+
)
4515+
4516+
hints_for_generic_function = get_type_hints(ann_module695.generic_function)
4517+
func_t_params = ann_module695.generic_function.__type_params__
4518+
self.assertEqual(
4519+
hints_for_generic_function.keys(), {"x", "y", "z", "zz", "return"}
4520+
)
4521+
self.assertIs(hints_for_generic_function["x"], func_t_params[0])
4522+
self.assertEqual(hints_for_generic_function["y"], Unpack[func_t_params[1]])
4523+
self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
4524+
self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])
4525+
45024526
def test_extended_generic_rules_subclassing(self):
45034527
class T1(Tuple[T, KT]): ...
45044528
class T2(Tuple[T, ...]): ...
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
from typing import Callable
3+
4+
5+
class A[T, *Ts, **P]:
6+
x: T
7+
y: tuple[*Ts]
8+
z: Callable[P, str]
9+
10+
11+
class B[T, *Ts, **P]:
12+
T = int
13+
Ts = str
14+
P = bytes
15+
x: T
16+
y: Ts
17+
z: P
18+
19+
20+
def generic_function[T, *Ts, **P](
21+
x: T, *y: *Ts, z: P.args, zz: P.kwargs
22+
) -> None: ...

Lib/typing.py

+27-8
Original file line numberDiff line numberDiff line change
@@ -403,15 +403,16 @@ def inner(*args, **kwds):
403403

404404
return decorator
405405

406-
def _eval_type(t, globalns, localns, recursive_guard=frozenset()):
406+
407+
def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()):
407408
"""Evaluate all forward references in the given type t.
408409
409410
For use of globalns and localns see the docstring for get_type_hints().
410411
recursive_guard is used to prevent infinite recursion with a recursive
411412
ForwardRef.
412413
"""
413414
if isinstance(t, ForwardRef):
414-
return t._evaluate(globalns, localns, recursive_guard)
415+
return t._evaluate(globalns, localns, type_params, recursive_guard=recursive_guard)
415416
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)):
416417
if isinstance(t, GenericAlias):
417418
args = tuple(
@@ -425,7 +426,13 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()):
425426
t = t.__origin__[args]
426427
if is_unpacked:
427428
t = Unpack[t]
428-
ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__)
429+
430+
ev_args = tuple(
431+
_eval_type(
432+
a, globalns, localns, type_params, recursive_guard=recursive_guard
433+
)
434+
for a in t.__args__
435+
)
429436
if ev_args == t.__args__:
430437
return t
431438
if isinstance(t, GenericAlias):
@@ -906,7 +913,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
906913
self.__forward_is_class__ = is_class
907914
self.__forward_module__ = module
908915

909-
def _evaluate(self, globalns, localns, recursive_guard):
916+
def _evaluate(self, globalns, localns, type_params, *, recursive_guard):
910917
if self.__forward_arg__ in recursive_guard:
911918
return self
912919
if not self.__forward_evaluated__ or localns is not globalns:
@@ -920,14 +927,25 @@ def _evaluate(self, globalns, localns, recursive_guard):
920927
globalns = getattr(
921928
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
922929
)
930+
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
923937
type_ = _type_check(
924-
eval(self.__forward_code__, globalns, localns),
938+
eval(self.__forward_code__, globalns, locals_to_pass),
925939
"Forward references must evaluate to types.",
926940
is_argument=self.__forward_is_argument__,
927941
allow_special_forms=self.__forward_is_class__,
928942
)
929943
self.__forward_value__ = _eval_type(
930-
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
944+
type_,
945+
globalns,
946+
localns,
947+
type_params,
948+
recursive_guard=(recursive_guard | {self.__forward_arg__}),
931949
)
932950
self.__forward_evaluated__ = True
933951
return self.__forward_value__
@@ -2241,7 +2259,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
22412259
value = type(None)
22422260
if isinstance(value, str):
22432261
value = ForwardRef(value, is_argument=False, is_class=True)
2244-
value = _eval_type(value, base_globals, base_locals)
2262+
value = _eval_type(value, base_globals, base_locals, base.__type_params__)
22452263
hints[name] = value
22462264
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
22472265

@@ -2267,6 +2285,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
22672285
raise TypeError('{!r} is not a module, class, method, '
22682286
'or function.'.format(obj))
22692287
hints = dict(hints)
2288+
type_params = getattr(obj, "__type_params__", ())
22702289
for name, value in hints.items():
22712290
if value is None:
22722291
value = type(None)
@@ -2278,7 +2297,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
22782297
is_argument=not isinstance(obj, types.ModuleType),
22792298
is_class=False,
22802299
)
2281-
hints[name] = _eval_type(value, globalns, localns)
2300+
hints[name] = _eval_type(value, globalns, localns, type_params)
22822301
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
22832302

22842303

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix erroneous :exc:`NameError` when calling :func:`typing.get_type_hints` on
2+
a class that made use of :pep:`695` type parameters in a module that had
3+
``from __future__ import annotations`` at the top of the file. Patch by Alex
4+
Waygood.

0 commit comments

Comments
 (0)