diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py new file mode 100644 index 00000000000000..d333db193183eb --- /dev/null +++ b/Lib/test/_typed_dict_helper.py @@ -0,0 +1,18 @@ +"""Used to test `get_type_hints()` on a cross-module inherited `TypedDict` class + +This script uses future annotations to postpone a type that won't be available +on the module inheriting from to `Foo`. The subclass in the other module should +look something like this: + + class Bar(_typed_dict_helper.Foo, total=False): + b: int +""" + +from __future__ import annotations + +from typing import Optional, TypedDict + +OptionalIntType = Optional[int] + +class Foo(TypedDict): + a: OptionalIntType diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e693883094d5c9..03e41193f44500 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -34,6 +34,7 @@ import types from test import mod_generics_cache +from test import _typed_dict_helper class BaseTestCase(TestCase): @@ -2804,6 +2805,9 @@ class Point2D(TypedDict): x: int y: int +class Bar(_typed_dict_helper.Foo, total=False): + b: int + class LabelPoint2D(Point2D, Label): ... class Options(TypedDict, total=False): @@ -3980,6 +3984,12 @@ def test_is_typeddict(self): # classes, not instances assert is_typeddict(Point2D()) is False + def test_get_type_hints(self): + self.assertEqual( + get_type_hints(Bar), + {'a': typing.Optional[int], 'b': int} + ) + class IOTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 2287f0521a364f..6db5ab614f6627 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -135,16 +135,16 @@ # legitimate imports of those modules. -def _type_convert(arg): +def _type_convert(arg, module=None): """For converting None to type(None), and strings to ForwardRef.""" if arg is None: return type(None) if isinstance(arg, str): - return ForwardRef(arg) + return ForwardRef(arg, module=module) return arg -def _type_check(arg, msg, is_argument=True): +def _type_check(arg, msg, is_argument=True, module=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 @@ -160,7 +160,7 @@ def _type_check(arg, msg, is_argument=True): if is_argument: invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) - arg = _type_convert(arg) + arg = _type_convert(arg, module=module) if (isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms): raise TypeError(f"{arg} is not valid as type argument") @@ -631,9 +631,9 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__') + '__forward_is_argument__', '__forward_module__') - def __init__(self, arg, is_argument=True): + def __init__(self, arg, is_argument=True, module=None): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") try: @@ -645,6 +645,7 @@ def __init__(self, arg, is_argument=True): self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument + self.__forward_module__ = module def _evaluate(self, globalns, localns, recursive_guard): if self.__forward_arg__ in recursive_guard: @@ -656,6 +657,10 @@ def _evaluate(self, globalns, localns, recursive_guard): globalns = localns elif localns is None: localns = globalns + if self.__forward_module__ is not None: + globalns = getattr( + sys.modules.get(self.__forward_module__, None), '__dict__', globalns + ) type_ =_type_check( eval(self.__forward_code__, globalns, localns), "Forward references must evaluate to types.", @@ -2234,7 +2239,8 @@ def __new__(cls, name, bases, ns, total=True): own_annotation_keys = set(own_annotations.keys()) msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" own_annotations = { - n: _type_check(tp, msg) for n, tp in own_annotations.items() + n: _type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own_annotations.items() } required_keys = set() optional_keys = set() diff --git a/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst b/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst new file mode 100644 index 00000000000000..06dae4a6e93565 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst @@ -0,0 +1,2 @@ +Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of +annotations across modules.