Skip to content

Commit

Permalink
gh-114053: Fix bad interaction of PEP-695, PEP-563 and ``get_type_hin…
Browse files Browse the repository at this point in the history
…ts`` (#118009)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
AlexWaygood and JelleZijlstra committed Apr 19, 2024
1 parent 15b3555 commit 1e3e7ce
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 10 deletions.
4 changes: 3 additions & 1 deletion Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3024,7 +3024,9 @@ Introspection helpers

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

Expand Down
26 changes: 25 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import types

from test.support import captured_stderr, cpython_only, infinite_recursion
from test.typinganndata import mod_generics_cache, _typed_dict_helper
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper


CANNOT_SUBCLASS_TYPE = 'Cannot subclass special typing classes'
Expand Down Expand Up @@ -4641,6 +4641,30 @@ def f(x: X): ...
{'x': list[list[ForwardRef('X')]]}
)

def test_pep695_generic_with_future_annotations(self):
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(
set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
set()
)

hints_for_generic_function = get_type_hints(ann_module695.generic_function)
func_t_params = ann_module695.generic_function.__type_params__
self.assertEqual(
hints_for_generic_function.keys(), {"x", "y", "z", "zz", "return"}
)
self.assertIs(hints_for_generic_function["x"], func_t_params[0])
self.assertEqual(hints_for_generic_function["y"], Unpack[func_t_params[1]])
self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])

def test_extended_generic_rules_subclassing(self):
class T1(Tuple[T, KT]): ...
class T2(Tuple[T, ...]): ...
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/typinganndata/ann_module695.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Callable


class A[T, *Ts, **P]:
x: T
y: tuple[*Ts]
z: Callable[P, str]


class B[T, *Ts, **P]:
T = int
Ts = str
P = bytes
x: T
y: Ts
z: P


def generic_function[T, *Ts, **P](
x: T, *y: *Ts, z: P.args, zz: P.kwargs
) -> None: ...
35 changes: 27 additions & 8 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,15 +399,16 @@ def inner(*args, **kwds):

return decorator

def _eval_type(t, globalns, localns, recursive_guard=frozenset()):

def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()):
"""Evaluate all forward references in the given type t.
For use of globalns and localns see the docstring for get_type_hints().
recursive_guard is used to prevent infinite recursion with a recursive
ForwardRef.
"""
if isinstance(t, ForwardRef):
return t._evaluate(globalns, localns, recursive_guard)
return t._evaluate(globalns, localns, type_params, recursive_guard=recursive_guard)
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)):
if isinstance(t, GenericAlias):
args = tuple(
Expand All @@ -421,7 +422,13 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()):
t = t.__origin__[args]
if is_unpacked:
t = Unpack[t]
ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__)

ev_args = tuple(
_eval_type(
a, globalns, localns, type_params, recursive_guard=recursive_guard
)
for a in t.__args__
)
if ev_args == t.__args__:
return t
if isinstance(t, GenericAlias):
Expand Down Expand Up @@ -974,7 +981,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
self.__forward_is_class__ = is_class
self.__forward_module__ = module

def _evaluate(self, globalns, localns, recursive_guard):
def _evaluate(self, globalns, localns, type_params, *, recursive_guard):
if self.__forward_arg__ in recursive_guard:
return self
if not self.__forward_evaluated__ or localns is not globalns:
Expand All @@ -988,14 +995,25 @@ def _evaluate(self, globalns, localns, recursive_guard):
globalns = getattr(
sys.modules.get(self.__forward_module__, None), '__dict__', 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
type_ = _type_check(
eval(self.__forward_code__, globalns, localns),
eval(self.__forward_code__, globalns, locals_to_pass),
"Forward references must evaluate to types.",
is_argument=self.__forward_is_argument__,
allow_special_forms=self.__forward_is_class__,
)
self.__forward_value__ = _eval_type(
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
type_,
globalns,
localns,
type_params,
recursive_guard=(recursive_guard | {self.__forward_arg__}),
)
self.__forward_evaluated__ = True
return self.__forward_value__
Expand Down Expand Up @@ -2334,7 +2352,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
value = type(None)
if isinstance(value, str):
value = ForwardRef(value, is_argument=False, is_class=True)
value = _eval_type(value, base_globals, base_locals)
value = _eval_type(value, base_globals, base_locals, base.__type_params__)
hints[name] = value
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}

Expand All @@ -2360,6 +2378,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
raise TypeError('{!r} is not a module, class, method, '
'or function.'.format(obj))
hints = dict(hints)
type_params = getattr(obj, "__type_params__", ())
for name, value in hints.items():
if value is None:
value = type(None)
Expand All @@ -2371,7 +2390,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
)
hints[name] = _eval_type(value, globalns, localns)
hints[name] = _eval_type(value, globalns, localns, type_params)
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}


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

0 comments on commit 1e3e7ce

Please sign in to comment.