]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-41249: Fix postponed annotations for TypedDict (GH-27017) (#27204)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 17 Jul 2021 08:48:17 +0000 (01:48 -0700)
committerGitHub <noreply@github.com>
Sat, 17 Jul 2021 08:48:17 +0000 (10:48 +0200)
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>
(cherry picked from commit 889036f7ef7290ef15b6c3373023f6a35387af0c)

Co-authored-by: Germán Méndez Bravo <german.mb@gmail.com>
Lib/test/_typed_dict_helper.py [new file with mode: 0644]
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst [new file with mode: 0644]

diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py
new file mode 100644 (file)
index 0000000..d333db1
--- /dev/null
@@ -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
index 07f809ca301e8cab5277ae268d2fb5ed4cf123cf..0c72784de6d0fbe7c5de580e21b1ffa233127e40 100644 (file)
@@ -33,6 +33,7 @@ import weakref
 import types
 
 from test import mod_generics_cache
+from test import _typed_dict_helper
 
 
 class BaseTestCase(TestCase):
@@ -2803,6 +2804,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):
@@ -3979,6 +3983,12 @@ class TypedDictTests(BaseTestCase):
         # 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):
 
index 2caa619c961b073a0865aa010016acef4a64cdb8..1823cb83ee915f7297838456fcd847eb4a9f6d96 100644 (file)
@@ -134,16 +134,16 @@ __all__ = [
 # 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
@@ -159,7 +159,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")
@@ -630,9 +630,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:
@@ -644,6 +644,7 @@ class ForwardRef(_Final, _root=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:
@@ -655,6 +656,10 @@ class ForwardRef(_Final, _root=True):
                 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.",
@@ -2233,7 +2238,8 @@ class _TypedDictMeta(type):
         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()
diff --git a/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst b/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst
new file mode 100644 (file)
index 0000000..06dae4a
--- /dev/null
@@ -0,0 +1,2 @@
+Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of
+annotations across modules.