Skip to content
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
17 changes: 15 additions & 2 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7172,6 +7172,19 @@ def func(x: MyClass['int'], y: MyClass[Annotated[int, ...]]): ...
assert isinstance(get_type_hints(func)['x'], MyAlias)
assert isinstance(get_type_hints(func)['y'], MyAlias)

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):
Expand Down Expand Up @@ -8657,8 +8670,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}
Expand Down
32 changes: 22 additions & 10 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,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
Expand All @@ -198,7 +198,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")
Expand Down Expand Up @@ -454,7 +454,7 @@ def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None:


def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset(),
format=None, owner=None, parent_fwdref=None):
format=None, owner=None, parent_fwdref=None, prefer_fwd_module=False):
Copy link
Member

@AlexWaygood AlexWaygood Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a description of this parameter to the docstring?

Should this default to True? Third-party users of this function may be expecting the legacy get_type_hints behaviour? They're obviously asking for breakage if they're using an undocumented, private implementation detail though. So I don't have a strong opinion here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the False behavior is more sensible so I want to default to it. I know this function gets used in the wild and we shouldn't go out of our way to break such uses, but at the end of the day it's a private function.

"""Evaluate all forward references in the given type t.

For use of globalns and localns see the docstring for get_type_hints().
Expand All @@ -464,8 +464,20 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()
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)
Expand All @@ -481,7 +493,7 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()
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 args
)
Expand Down Expand Up @@ -2369,7 +2381,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
Expand Down Expand Up @@ -2414,7 +2426,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
Expand Down Expand Up @@ -3111,7 +3123,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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :func:`typing.get_type_hints` calls on generic :class:`typing.TypedDict`
classes defined with string annotations.
Loading