bpo-41249: Fix postponed annotations for TypedDict (GH-27017)

This fixes TypedDict to work with get_type_hints and postponed evaluation of annotations across modules.

This is done by adding the module name to ForwardRef at the time the object is created and using that to resolve the globals during the evaluation.

Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
This commit is contained in:
Germán Méndez Bravo 2021-07-16 20:49:30 -07:00 committed by GitHub
parent bf89ff96e6
commit 889036f7ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 7 deletions

View file

@ -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

View file

@ -34,6 +34,7 @@
import types
from test import mod_generics_cache
from test import _typed_dict_helper
class BaseTestCase(TestCase):
@ -2819,6 +2820,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):
@ -3995,6 +3999,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):

View file

@ -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")
@ -633,9 +633,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:
@ -647,6 +647,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:
@ -658,6 +659,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.",
@ -2242,7 +2247,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()

View file

@ -0,0 +1,2 @@
Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of
annotations across modules.