Skip to content

Commit 32179f2

Browse files
committed
Fix postponed annotations for TypedDict
This fixes TypedDict to work with get_type_hints and postponed evaluation of annotations across modules.
1 parent bc39614 commit 32179f2

File tree

4 files changed

+44
-7
lines changed

4 files changed

+44
-7
lines changed

Lib/test/test_typing.py

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import types
3535

3636
from test import mod_generics_cache
37+
from test import typed_dict
3738

3839

3940
class BaseTestCase(TestCase):
@@ -2804,6 +2805,9 @@ class Point2D(TypedDict):
28042805
x: int
28052806
y: int
28062807

2808+
class Bar(typed_dict.Foo, total=False):
2809+
b: int
2810+
28072811
class LabelPoint2D(Point2D, Label): ...
28082812

28092813
class Options(TypedDict, total=False):
@@ -3980,6 +3984,12 @@ def test_is_typeddict(self):
39803984
# classes, not instances
39813985
assert is_typeddict(Point2D()) is False
39823986

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

39843994
class IOTests(BaseTestCase):
39853995

Lib/test/typed_dict.py

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

Lib/typing.py

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

137137

138-
def _type_convert(arg):
138+
def _type_convert(arg, module=None):
139139
"""For converting None to type(None), and strings to ForwardRef."""
140140
if arg is None:
141141
return type(None)
142142
if isinstance(arg, str):
143-
return ForwardRef(arg)
143+
return ForwardRef(arg, module=module)
144144
return arg
145145

146146

147-
def _type_check(arg, msg, is_argument=True):
147+
def _type_check(arg, msg, is_argument=True, module=None):
148148
"""Check that the argument is a type, and return it (internal helper).
149149
150150
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):
160160
if is_argument:
161161
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
162162

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

632632
__slots__ = ('__forward_arg__', '__forward_code__',
633633
'__forward_evaluated__', '__forward_value__',
634-
'__forward_is_argument__')
634+
'__forward_is_argument__', '__forward_module__')
635635

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

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

0 commit comments

Comments
 (0)