mirror of
https://github.com/python/cpython.git
synced 2025-12-31 04:23:37 +00:00
[3.14] gh-137226: Fix get_type_hints() on generic TypedDict with stringified annotations (GH-138953) (#138989)
This commit is contained in:
parent
82fce8271c
commit
6038447d99
3 changed files with 39 additions and 12 deletions
|
|
@ -7132,6 +7132,19 @@ def add_right(self, node: 'Node[T]' = None):
|
|||
right_hints = get_type_hints(t.add_right, globals(), locals())
|
||||
self.assertEqual(right_hints['node'], Node[T])
|
||||
|
||||
def test_stringified_typeddict(self):
|
||||
ns = run_code(
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import TypedDict
|
||||
class TD[UniqueT](TypedDict):
|
||||
a: UniqueT
|
||||
"""
|
||||
)
|
||||
TD = ns['TD']
|
||||
self.assertEqual(TD.__annotations__, {'a': EqualToForwardRef('UniqueT', owner=TD, module=TD.__module__)})
|
||||
self.assertEqual(get_type_hints(TD), {'a': TD.__type_params__[0]})
|
||||
|
||||
|
||||
class GetUtilitiesTestCase(TestCase):
|
||||
def test_get_origin(self):
|
||||
|
|
@ -8678,8 +8691,8 @@ def _make_td(future, class_name, annos, base, extra_names=None):
|
|||
child = _make_td(
|
||||
child_future, "Child", {"child": "int"}, "Base", {"Base": base}
|
||||
)
|
||||
base_anno = ForwardRef("int", module="builtins") if base_future else int
|
||||
child_anno = ForwardRef("int", module="builtins") if child_future else int
|
||||
base_anno = ForwardRef("int", module="builtins", owner=base) if base_future else int
|
||||
child_anno = ForwardRef("int", module="builtins", owner=child) if child_future else int
|
||||
self.assertEqual(base.__annotations__, {'base': base_anno})
|
||||
self.assertEqual(
|
||||
child.__annotations__, {'child': child_anno, 'base': base_anno}
|
||||
|
|
|
|||
|
|
@ -172,16 +172,16 @@ def __getattr__(self, attr):
|
|||
_lazy_annotationlib = _LazyAnnotationLib()
|
||||
|
||||
|
||||
def _type_convert(arg, module=None, *, allow_special_forms=False):
|
||||
def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None):
|
||||
"""For converting None to type(None), and strings to ForwardRef."""
|
||||
if arg is None:
|
||||
return type(None)
|
||||
if isinstance(arg, str):
|
||||
return _make_forward_ref(arg, module=module, is_class=allow_special_forms)
|
||||
return _make_forward_ref(arg, module=module, is_class=allow_special_forms, owner=owner)
|
||||
return arg
|
||||
|
||||
|
||||
def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=False):
|
||||
def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=False, owner=None):
|
||||
"""Check that the argument is a type, and return it (internal helper).
|
||||
|
||||
As a special case, accept None and return type(None) instead. Also wrap strings
|
||||
|
|
@ -199,7 +199,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=
|
|||
if is_argument:
|
||||
invalid_generic_forms += (Final,)
|
||||
|
||||
arg = _type_convert(arg, module=module, allow_special_forms=allow_special_forms)
|
||||
arg = _type_convert(arg, module=module, allow_special_forms=allow_special_forms, owner=owner)
|
||||
if (isinstance(arg, _GenericAlias) and
|
||||
arg.__origin__ in invalid_generic_forms):
|
||||
raise TypeError(f"{arg} is not valid as type argument")
|
||||
|
|
@ -431,7 +431,7 @@ def __repr__(self):
|
|||
|
||||
|
||||
def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(),
|
||||
format=None, owner=None, parent_fwdref=None):
|
||||
format=None, owner=None, parent_fwdref=None, prefer_fwd_module=False):
|
||||
"""Evaluate all forward references in the given type t.
|
||||
|
||||
For use of globalns and localns see the docstring for get_type_hints().
|
||||
|
|
@ -444,8 +444,20 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f
|
|||
if isinstance(t, _lazy_annotationlib.ForwardRef):
|
||||
# If the forward_ref has __forward_module__ set, evaluate() infers the globals
|
||||
# from the module, and it will probably pick better than the globals we have here.
|
||||
if t.__forward_module__ is not None:
|
||||
# We do this only for calls from get_type_hints() (which opts in through the
|
||||
# prefer_fwd_module flag), so that the default behavior remains more straightforward.
|
||||
if prefer_fwd_module and t.__forward_module__ is not None:
|
||||
globalns = None
|
||||
# If there are type params on the owner, we need to add them back, because
|
||||
# annotationlib won't.
|
||||
if owner_type_params := getattr(owner, "__type_params__", None):
|
||||
globalns = getattr(
|
||||
sys.modules.get(t.__forward_module__, None), "__dict__", None
|
||||
)
|
||||
if globalns is not None:
|
||||
globalns = dict(globalns)
|
||||
for type_param in owner_type_params:
|
||||
globalns[type_param.__name__] = type_param
|
||||
return evaluate_forward_ref(t, globals=globalns, locals=localns,
|
||||
type_params=type_params, owner=owner,
|
||||
_recursive_guard=recursive_guard, format=format)
|
||||
|
|
@ -466,7 +478,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f
|
|||
ev_args = tuple(
|
||||
_eval_type(
|
||||
a, globalns, localns, type_params, recursive_guard=recursive_guard,
|
||||
format=format, owner=owner,
|
||||
format=format, owner=owner, prefer_fwd_module=prefer_fwd_module,
|
||||
)
|
||||
for a in t.__args__
|
||||
)
|
||||
|
|
@ -2363,7 +2375,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
|
|||
if isinstance(value, str):
|
||||
value = _make_forward_ref(value, is_argument=False, is_class=True)
|
||||
value = _eval_type(value, base_globals, base_locals, (),
|
||||
format=format, owner=obj)
|
||||
format=format, owner=obj, prefer_fwd_module=True)
|
||||
if value is None:
|
||||
value = type(None)
|
||||
hints[name] = value
|
||||
|
|
@ -2408,7 +2420,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
|
|||
is_argument=not isinstance(obj, types.ModuleType),
|
||||
is_class=False,
|
||||
)
|
||||
value = _eval_type(value, globalns, localns, (), format=format, owner=obj)
|
||||
value = _eval_type(value, globalns, localns, (), format=format, owner=obj, prefer_fwd_module=True)
|
||||
if value is None:
|
||||
value = type(None)
|
||||
hints[name] = value
|
||||
|
|
@ -3147,7 +3159,7 @@ def __new__(cls, name, bases, ns, total=True):
|
|||
own_annotations = {}
|
||||
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
|
||||
own_checked_annotations = {
|
||||
n: _type_check(tp, msg, module=tp_dict.__module__)
|
||||
n: _type_check(tp, msg, owner=tp_dict, module=tp_dict.__module__)
|
||||
for n, tp in own_annotations.items()
|
||||
}
|
||||
required_keys = set()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Fix :func:`typing.get_type_hints` calls on generic :class:`typing.TypedDict`
|
||||
classes defined with string annotations.
|
||||
Loading…
Add table
Add a link
Reference in a new issue