From 32179f22bab0988a5a760f2716c9ce5ae09219da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=C3=A9ndez=20Bravo?= Date: Sun, 4 Jul 2021 09:38:28 -0700 Subject: [PATCH 1/6] Fix postponed annotations for TypedDict This fixes TypedDict to work with get_type_hints and postponed evaluation of annotations across modules. --- Lib/test/test_typing.py | 10 ++++++++++ Lib/test/typed_dict.py | 19 ++++++++++++++++++ Lib/typing.py | 20 ++++++++++++------- .../2021-07-04-11-33-34.bpo-41249.sHdwBE.rst | 2 ++ 4 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 Lib/test/typed_dict.py create mode 100644 Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e693883094d5c9..1a2c28977d3e12 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -34,6 +34,7 @@ import types from test import mod_generics_cache +from test import typed_dict class BaseTestCase(TestCase): @@ -2804,6 +2805,9 @@ class Point2D(TypedDict): x: int y: int +class Bar(typed_dict.Foo, total=False): + b: int + class LabelPoint2D(Point2D, Label): ... class Options(TypedDict, total=False): @@ -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): diff --git a/Lib/test/typed_dict.py b/Lib/test/typed_dict.py new file mode 100644 index 00000000000000..6ab3c7b4606cdc --- /dev/null +++ b/Lib/test/typed_dict.py @@ -0,0 +1,19 @@ +"""Script used to test `get_type_hints()` on a class formed by inheriting +a `TypedDict` with postponed annotations, for bpo-41249, pull request #27017. + +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.Foo, total=False): + b: int +""" + +from __future__ import annotations + +from typing import Optional, TypedDict + +OptionalIntType = Optional[int] + +class Foo(TypedDict): + a: OptionalIntType diff --git a/Lib/typing.py b/Lib/typing.py index 2287f0521a364f..6db5ab614f6627 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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 @@ -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") @@ -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: @@ -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: @@ -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.", @@ -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() 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 index 00000000000000..a83bdbe2056a7c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst @@ -0,0 +1,2 @@ +Fixes TypedDict to work with get_type_hints() and postponed evaluation of +annotations across modules. From 387f16e4ddff94afc6eab9d76a7f9e0f8a9ffce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=C3=A9ndez=20Bravo?= Date: Wed, 7 Jul 2021 09:52:07 -0700 Subject: [PATCH 2/6] Remove pull request number from comment Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> --- Lib/test/typed_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/typed_dict.py b/Lib/test/typed_dict.py index 6ab3c7b4606cdc..28132669d12b1e 100644 --- a/Lib/test/typed_dict.py +++ b/Lib/test/typed_dict.py @@ -1,5 +1,5 @@ """Script used to test `get_type_hints()` on a class formed by inheriting -a `TypedDict` with postponed annotations, for bpo-41249, pull request #27017. +a `TypedDict` with postponed annotations, for bpo-41249. 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 From 846b7c223d8895f92e35e01e08f8469bd062706a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=C3=A9ndez=20Bravo?= Date: Wed, 7 Jul 2021 09:52:39 -0700 Subject: [PATCH 3/6] Formatting in the NEWS Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> --- .../next/Library/2021-07-04-11-33-34.bpo-41249.sHdwBE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a83bdbe2056a7c..06dae4a6e93565 100644 --- 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 @@ -1,2 +1,2 @@ -Fixes TypedDict to work with get_type_hints() and postponed evaluation of +Fixes ``TypedDict`` to work with ``typing.get_type_hints()`` and postponed evaluation of annotations across modules. From fa412f32fbd14dddc1cccd0a60db9de89e578b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=C3=A9ndez=20Bravo?= Date: Wed, 7 Jul 2021 19:44:03 -0700 Subject: [PATCH 4/6] Change summary and test helper file --- Lib/test/{typed_dict.py => _typed_dict_helper.py} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename Lib/test/{typed_dict.py => _typed_dict_helper.py} (75%) diff --git a/Lib/test/typed_dict.py b/Lib/test/_typed_dict_helper.py similarity index 75% rename from Lib/test/typed_dict.py rename to Lib/test/_typed_dict_helper.py index 28132669d12b1e..b52f34eea10874 100644 --- a/Lib/test/typed_dict.py +++ b/Lib/test/_typed_dict_helper.py @@ -1,5 +1,4 @@ -"""Script used to test `get_type_hints()` on a class formed by inheriting -a `TypedDict` with postponed annotations, for bpo-41249. +"""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 From 659fba33d9f4a4a345bddccd133561f259dc5e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=C3=A9ndez=20Bravo?= Date: Thu, 8 Jul 2021 09:59:22 -0700 Subject: [PATCH 5/6] Fix module import --- Lib/test/test_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 1a2c28977d3e12..03e41193f44500 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -34,7 +34,7 @@ import types from test import mod_generics_cache -from test import typed_dict +from test import _typed_dict_helper class BaseTestCase(TestCase): @@ -2805,7 +2805,7 @@ class Point2D(TypedDict): x: int y: int -class Bar(typed_dict.Foo, total=False): +class Bar(_typed_dict_helper.Foo, total=False): b: int class LabelPoint2D(Point2D, Label): ... From 1ed104a4f2898a4d327cbc102c571439295bf142 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 16 Jul 2021 20:23:44 -0700 Subject: [PATCH 6/6] Update module name in docstring. --- Lib/test/_typed_dict_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py index b52f34eea10874..d333db193183eb 100644 --- a/Lib/test/_typed_dict_helper.py +++ b/Lib/test/_typed_dict_helper.py @@ -4,7 +4,7 @@ on the module inheriting from to `Foo`. The subclass in the other module should look something like this: - class Bar(typed_dict.Foo, total=False): + class Bar(_typed_dict_helper.Foo, total=False): b: int """