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 bad interaction of PEP-695, PEP-563 and get_type_hints #118009

Merged
merged 4 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading