Skip to content

bpo-41249: Fix postponed annotations for TypedDict #27017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Lib/test/_typed_dict_helper.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import types

from test import mod_generics_cache
from test import _typed_dict_helper


class BaseTestCase(TestCase):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Expand Down
20 changes: 13 additions & 7 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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.",
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of
annotations across modules.