From 5430f614371530aab1178b3b610add4bf54c383e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 19 Apr 2024 14:41:28 +0100 Subject: [3.12] gh-114053: Fix bad interaction of PEP-695, PEP-563 and ``get_type_hints`` (#118009) (#118104) Co-authored-by: Jelle Zijlstra --- Doc/library/typing.rst | 4 ++- Lib/test/test_typing.py | 26 +++++++++++++++- Lib/test/typinganndata/ann_module695.py | 22 ++++++++++++++ Lib/typing.py | 35 +++++++++++++++++----- .../2024-04-17-22-00-15.gh-issue-114053._JBV4D.rst | 4 +++ 5 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 Lib/test/typinganndata/ann_module695.py create mode 100644 Misc/NEWS.d/next/Library/2024-04-17-22-00-15.gh-issue-114053._JBV4D.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 57525b8..0edd5f7 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -2887,7 +2887,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 ` namespaces. + For a class ``C``, return a dictionary constructed by merging all the ``__annotations__`` along ``C.__mro__`` in reverse order. diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index dc117b3..e7cb01a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -47,7 +47,7 @@ import weakref import types from test.support import captured_stderr, cpython_only -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' @@ -4499,6 +4499,30 @@ class GenericTests(BaseTestCase): {'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, ...]): ... diff --git a/Lib/test/typinganndata/ann_module695.py b/Lib/test/typinganndata/ann_module695.py new file mode 100644 index 0000000..2ede9fe --- /dev/null +++ b/Lib/test/typinganndata/ann_module695.py @@ -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: ... diff --git a/Lib/typing.py b/Lib/typing.py index b58c2d3..82d05f4 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -403,7 +403,8 @@ def _tp_cache(func=None, /, *, typed=False): 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(). @@ -411,7 +412,7 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): 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( @@ -425,7 +426,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): @@ -906,7 +913,7 @@ class ForwardRef(_Final, _root=True): 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: @@ -920,14 +927,25 @@ class ForwardRef(_Final, _root=True): 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__ @@ -2241,7 +2259,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()} @@ -2267,6 +2285,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) @@ -2278,7 +2297,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()} diff --git a/Misc/NEWS.d/next/Library/2024-04-17-22-00-15.gh-issue-114053._JBV4D.rst b/Misc/NEWS.d/next/Library/2024-04-17-22-00-15.gh-issue-114053._JBV4D.rst new file mode 100644 index 0000000..827b2d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-17-22-00-15.gh-issue-114053._JBV4D.rst @@ -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. -- cgit v0.12