8000 [3.9] bpo-41249: Fix postponed annotations for TypedDict (GH-27017) (… · python/cpython@fa674bd · GitHub
[go: up one dir, main page]

Skip to content

Commit fa674bd

Browse files
Fidget-SpinnerKronuzambv
authored
[3.9] bpo-41249: Fix postponed annotations for TypedDict (GH-27017) (GH-27205)
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> Co-authored-by: Germán Méndez Bravo <german.mb@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent df7c629 commit fa674bd

File tree

4 files changed

+43
-7
lines changed

4 files changed

+43
-7
lines changed

Lib/test/_typed_dict_helper.py

Lines changed: 18 additions & 0 deletions
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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import types
3030

3131
from test import mod_generics_cache
32+
from test import _typed_dict_helper
3233

3334

3435
class BaseTestCase(TestCase):
@@ -2808,6 +2809,9 @@ class Point2D(TypedDict):
28082809
x: int
28092810
y: int
28102811

2812+
class Bar(_typed_dict_helper.Foo, total=False):
2813+
b: int
2814+
28112815
class LabelPoint2D(Point2D, Label): ...
28122816

28132817
class Options(TypedDict, total=False):
@@ -3944,6 +3948,12 @@ class Cat(Animal):
39443948
'voice': str,
39453949
}
39463950

3951+
def test_get_type_hints(self):
3952+
self.assertEqual(
3953+
get_type_hints(Bar),
3954+
{'a': typing.Optional[int], 'b': int}
3955+
)
3956+
39473957

39483958
class IOTests(BaseTestCase):
39493959

Lib/typing.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,16 @@
125125
# legitimate imports of those modules.
126126

127127

128-
def _type_convert(arg):
128+
def _type_convert(arg, module=None):
129129
"""For converting None to type(None), and strings to ForwardRef."""
130130
if arg is None:
131131
return type(None)
132132
if isinstance(arg, str):
133-
return ForwardRef(arg)
133+
return ForwardRef(arg, module=module)
134134
return arg
135135

136136

137-
def _type_check(arg, msg, is_argument=True):
137+
def _type_check(arg, msg, is_argument=True, module=None):
138138
"""Check that the argument is a type, and return it (internal helper).
139139
140140
As a special case, accept None and return type(None) instead. Also wrap strings
@@ -150,7 +150,7 @@ def _type_check(arg, msg, is_argument=True):
150150
if is_argument:
151151
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
152152

153-
arg = _type_convert(arg)
153+
arg = _type_convert(arg, module=module)
154154
if (isinstance(arg, _GenericAlias) and
155155
arg.__origin__ in invalid_generic_forms):
156156
raise TypeError(f"{arg} is not valid as type argument")
@@ -517,9 +517,9 @@ class ForwardRef(_Final, _root=True):
517517

518518
__slots__ = ('__forward_arg__', '__forward_code__',
519519
'__forward_evaluated__', '__forward_value__',
520-
'__forward_is_argument__')
520+
'__forward_is_argument__', '__forward_module__')
521521

522-
def __init__(self, arg, is_argument=True):
522+
def __init__(self, arg, is_argument=True, module=None):
523523
if not isinstance(arg, str):
524524
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
525525
try:
@@ -531,6 +531,7 @@ def __init__(self, arg, is_argument=True):
531531
self.__forward_evaluated__ = False
532532
self.__forward_value__ = None
533533
self.__forward_is_argument__ = is_argument
534+
self.__forward_module__ = module
534535

535536
def _evaluate(self, globalns, localns, recursive_guard):
536537
if self.__forward_arg__ in recursive_guard:
@@ -542,6 +543,10 @@ def _evaluate(self, globalns, localns, recursive_guard):
542543
globalns = localns
543544
elif localns is None:
544545
localns = globalns
546+
if self.__forward_module__ is not None:
547+
globalns = getattr(
548+
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
549+
)
545550
type_ =_type_check(
546551
eval(self.__forward_code__, globalns, localns),
547552
"Forward references must evaluate to types.",
@@ -1912,7 +1917,8 @@ def __new__(cls, name, bases, ns, total=True):
19121917
own_annotation_keys = set(own_annotations.keys())
19131918
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
19141919
own_annotations = {
1915-
n: _type_check(tp, msg) for n, tp in own_annotations.items()
1920+
n: _type_check(tp, msg, module=tp_dict.__module__)
1921+
for n, tp in own_annotations.items()
19161922
}
19171923
required_keys = set()
19181924
optional_keys = set()
Lines changed: 2 additions & 0 deletions
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)
0