Skip to content

Commit 480f29f

Browse files
bpo-41249: Fix postponed annotations for TypedDict (GH-27017) (#27204)
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 <[email protected]> (cherry picked from commit 889036f) Co-authored-by: Germán Méndez Bravo <[email protected]>
1 parent efda905 commit 480f29f

File tree

4 files changed

+43
-7
lines changed

4 files changed

+43
-7
lines changed

Lib/test/_typed_dict_helper.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Used to test `get_type_hints()` on a cross-module inherited `TypedDict` class
2+
3+
This script uses future annotations to postpone a type that won't be available
4+
on the module inheriting from to `Foo`. The subclass in the other module should
5+
look something like this:
6+
7+
class Bar(_typed_dict_helper.Foo, total=False):
8+
b: int
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import Optional, TypedDict
14+
15+
OptionalIntType = Optional[int]
16+
17+
class Foo(TypedDict):
18+
a: OptionalIntType

Lib/test/test_typing.py

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import types
3434

3535
from test import mod_generics_cache
36+
from test import _typed_dict_helper
3637

3738

3839
class BaseTestCase(TestCase):
@@ -2803,6 +2804,9 @@ class Point2D(TypedDict):
28032804
x: int
28042805
y: int
28052806

2807+
class Bar(_typed_dict_helper.Foo, total=False):
2808+
b: int
2809+
28062810
class LabelPoint2D(Point2D, Label): ...
28072811

28082812
class Options(TypedDict, total=False):
@@ -3979,6 +3983,12 @@ def test_is_typeddict(self):
39793983
# classes, not instances
39803984
assert is_typeddict(Point2D()) is False
39813985

3986+
def test_get_type_hints(self):
3987+
self.assertEqual(
3988+
get_type_hints(Bar),
3989+
{'a': typing.Optional[int], 'b': int}
3990+
)
3991+
39823992

39833993
class IOTests(BaseTestCase):
39843994

Lib/typing.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,16 @@
134134
# legitimate imports of those modules.
135135

136136

137-
def _type_convert(arg):
137+
def _type_convert(arg, module=None):
138138
"""For converting None to type(None), and strings to ForwardRef."""
139139
if arg is None:
140140
return type(None)
141141
if isinstance(arg, str):
142-
return ForwardRef(arg)
142+
return ForwardRef(arg, module=module)
143143
return arg
144144

145145

146-
def _type_check(arg, msg, is_argument=True):
146+
def _type_check(arg, msg, is_argument=True, module=None):
147147
"""Check that the argument is a type, and return it (internal helper).
148148
149149
As a special case, accept None and return type(None) instead. Also wrap strings
@@ -159,7 +159,7 @@ def _type_check(arg, msg, is_argument=True):
159159
if is_argument:
160160
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
161161

162-
arg = _type_convert(arg)
162+
arg = _type_convert(arg, module=module)
163163
if (isinstance(arg, _GenericAlias) and
164164
arg.__origin__ in invalid_generic_forms):
165165
raise TypeError(f"{arg} is not valid as type argument")
@@ -630,9 +630,9 @@ class ForwardRef(_Final, _root=True):
630630

631631
__slots__ = ('__forward_arg__', '__forward_code__',
632632
'__forward_evaluated__', '__forward_value__',
633-
'__forward_is_argument__')
633+
'__forward_is_argument__', '__forward_module__')
634634

635-
def __init__(self, arg, is_argument=True):
635+
def __init__(self, arg, is_argument=True, module=None):
636636
if not isinstance(arg, str):
637637
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
638638
try:
@@ -644,6 +644,7 @@ def __init__(self, arg, is_argument=True):
644644
self.__forward_evaluated__ = False
645645
self.__forward_value__ = None
646646
self.__forward_is_argument__ = is_argument
647+
self.__forward_module__ = module
647648

648649
def _evaluate(self, globalns, localns, recursive_guard):
649650
if self.__forward_arg__ in recursive_guard:
@@ -655,6 +656,10 @@ def _evaluate(self, globalns, localns, recursive_guard):
655656
globalns = localns
656657
elif localns is None:
657658
localns = globalns
659+
if self.__forward_module__ is not None:
660+
globalns = getattr(
661+
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
662+
)
658663
type_ =_type_check(
659664
eval(self.__forward_code__, globalns, localns),
660665
"Forward references must evaluate to types.",
@@ -2233,7 +2238,8 @@ def __new__(cls, name, bases, ns, total=True):
22332238
own_annotation_keys = set(own_annotations.keys())
22342239
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
22352240
own_annotations = {
2236-
n: _type_check(tp, msg) for n, tp in own_annotations.items()
2241+
n: _type_check(tp, msg, module=tp_dict.__module__)
2242+
for n, tp in own_annotations.items()
22372243
}
22382244
required_keys = set()
22392245
optional_keys = set()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of
2+
annotations across modules.

0 commit comments

Comments
 (0)