From 1d46e26aed40340a0f4a3442ec0cb9d431f139f5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 10 Sep 2021 21:36:16 +0300 Subject: [PATCH 1/8] bpo-45166: fixes `get_type_hints` failure on `Final` --- Lib/test/ann_module5.py | 8 ++++++++ Lib/test/test_typing.py | 10 +++++++++- Lib/typing.py | 5 +++-- .../Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 Lib/test/ann_module5.py create mode 100644 Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst diff --git a/Lib/test/ann_module5.py b/Lib/test/ann_module5.py new file mode 100644 index 00000000000000..1a413464b69896 --- /dev/null +++ b/Lib/test/ann_module5.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from typing import Final + +name: Final[str] = "final" + +class MyClass: + value: Final = 3000 diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fa49b90886c302..8b3edc83a97d7c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2975,7 +2975,7 @@ async def __aexit__(self, etype, eval, tb): # Definitions needed for features introduced in Python 3.6 -from test import ann_module, ann_module2, ann_module3 +from test import ann_module, ann_module2, ann_module3, ann_module5 from typing import AsyncContextManager class A: @@ -3339,6 +3339,14 @@ class C(Generic[T]): pass (Concatenate[int, P], int)) self.assertEqual(get_args(list | str), (list, str)) + def test_forward_ref_and_final(self): + # https://bugs.python.org/issue45166 + hints = get_type_hints(ann_module5) + self.assertEqual(hints, {'name': Final[str]}) + + hints = get_type_hints(ann_module5.MyClass) + self.assertEqual(hints, {'value': Final}) + class CollectionsAbcTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index e29d699283dfec..112209f1cb2d0f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -171,7 +171,7 @@ def _type_check(arg, msg, is_argument=True, module=None): if (isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms): raise TypeError(f"{arg} is not valid as type argument") - if arg in (Any, NoReturn): + if arg in (Any, NoReturn, Final): return arg if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol): raise TypeError(f"Plain {arg} is not valid as type argument") @@ -1831,7 +1831,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if value is None: value = type(None) if isinstance(value, str): - value = ForwardRef(value) + value = ForwardRef( + value, is_argument=not isinstance(obj, types.ModuleType)) value = _eval_type(value, globalns, localns) if name in defaults and defaults[name] is None: value = Optional[value] diff --git a/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst b/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst new file mode 100644 index 00000000000000..21f16d14b90420 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst @@ -0,0 +1,2 @@ +Fixes that ``Final`` wrapped in ``ForwardRef`` was failing during +``get_type_hints``. From e121641c0195500e5a7c6ea5da4ce6f538e2ee4d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 23 Sep 2021 23:58:50 +0300 Subject: [PATCH 2/8] Adds `is_class` prototype to `ForwardRef` in `typing` --- Lib/test/ann_module6.py | 7 +++++++ Lib/test/test_typing.py | 10 +++++++++- Lib/typing.py | 22 +++++++++++++++------- 3 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 Lib/test/ann_module6.py diff --git a/Lib/test/ann_module6.py b/Lib/test/ann_module6.py new file mode 100644 index 00000000000000..679175669bc3ac --- /dev/null +++ b/Lib/test/ann_module6.py @@ -0,0 +1,7 @@ +# Tests that top-level ClassVar is not allowed + +from __future__ import annotations + +from typing import ClassVar + +wrong: ClassVar[int] = 1 diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8b3edc83a97d7c..d1887f7eee4437 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2975,7 +2975,7 @@ async def __aexit__(self, etype, eval, tb): # Definitions needed for features introduced in Python 3.6 -from test import ann_module, ann_module2, ann_module3, ann_module5 +from test import ann_module, ann_module2, ann_module3, ann_module5, ann_module6 from typing import AsyncContextManager class A: @@ -3347,6 +3347,14 @@ def test_forward_ref_and_final(self): hints = get_type_hints(ann_module5.MyClass) self.assertEqual(hints, {'value': Final}) + def test_top_level_class_var(self): + # https://bugs.python.org/issue45166 + with self.assertRaisesRegex( + TypeError, + r'typing.ClassVar\[int\] is not valid as type argument', + ): + get_type_hints(ann_module6) + class CollectionsAbcTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 112209f1cb2d0f..53a89d140ebf01 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -151,7 +151,7 @@ def _type_convert(arg, module=None): return arg -def _type_check(arg, msg, is_argument=True, module=None): +def _type_check(arg, msg, is_argument=True, is_class=False, 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 @@ -164,8 +164,10 @@ def _type_check(arg, msg, is_argument=True, module=None): We append the repr() of the actual value (truncated to 100 chars). """ invalid_generic_forms = (Generic, Protocol) - if is_argument: + if is_argument and not is_class: invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) + elif not is_argument and not is_class: # module / local level + invalid_generic_forms = invalid_generic_forms + (ClassVar, ) arg = _type_convert(arg, module=module) if (isinstance(arg, _GenericAlias) and @@ -662,9 +664,10 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__', '__forward_module__') + '__forward_is_argument__', '__forward_is_class__', + '__forward_module__') - def __init__(self, arg, is_argument=True, module=None): + def __init__(self, arg, is_argument=True, is_class=False, module=None): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") try: @@ -676,6 +679,7 @@ def __init__(self, arg, is_argument=True, module=None): self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument + self.__forward_is_class__ = is_class self.__forward_module__ = module def _evaluate(self, globalns, localns, recursive_guard): @@ -692,10 +696,11 @@ def _evaluate(self, globalns, localns, recursive_guard): globalns = getattr( sys.modules.get(self.__forward_module__, None), '__dict__', globalns ) - type_ =_type_check( + type_ = _type_check( eval(self.__forward_code__, globalns, localns), "Forward references must evaluate to types.", is_argument=self.__forward_is_argument__, + is_class=self.__forward_is_class__, ) self.__forward_value__ = _eval_type( type_, globalns, localns, recursive_guard | {self.__forward_arg__} @@ -1799,7 +1804,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if value is None: value = type(None) if isinstance(value, str): - value = ForwardRef(value, is_argument=False) + value = ForwardRef(value, is_argument=False, is_class=True) value = _eval_type(value, base_globals, base_locals) hints[name] = value return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} @@ -1832,7 +1837,10 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = type(None) if isinstance(value, str): value = ForwardRef( - value, is_argument=not isinstance(obj, types.ModuleType)) + value, + is_argument=not isinstance(obj, types.ModuleType), + is_class=False, + ) value = _eval_type(value, globalns, localns) if name in defaults and defaults[name] is None: value = Optional[value] From 9752a76a9bb7e87acf289ab0e79a1a9342cd4956 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 24 Sep 2021 14:28:03 +0300 Subject: [PATCH 3/8] Update Lib/typing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 53a89d140ebf01..d15c3dca9cdfce 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -151,7 +151,7 @@ def _type_convert(arg, module=None): return arg -def _type_check(arg, msg, is_argument=True, is_class=False, module=None): +def _type_check(arg, msg, is_argument=True, module=None, *, is_class=False): """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 From ff0deb501fe5b16ee1a04f46cfbd00be022a61f8 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 24 Sep 2021 14:28:10 +0300 Subject: [PATCH 4/8] Update Lib/typing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/typing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index d15c3dca9cdfce..1162c1546c872a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -164,10 +164,10 @@ def _type_check(arg, msg, is_argument=True, module=None, *, is_class=False): We append the repr() of the actual value (truncated to 100 chars). """ invalid_generic_forms = (Generic, Protocol) - if is_argument and not is_class: - invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) - elif not is_argument and not is_class: # module / local level - invalid_generic_forms = invalid_generic_forms + (ClassVar, ) + if not is_class: + invalid_generic_forms += (ClassVar,) + if is_argument: + invalid_generic_forms += (Final,) arg = _type_convert(arg, module=module) if (isinstance(arg, _GenericAlias) and From 7a343361ba017a08f8fc3dc1621f4a34d69b9181 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 24 Sep 2021 14:28:15 +0300 Subject: [PATCH 5/8] Update Lib/typing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 1162c1546c872a..fd92f85d70ff4c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1836,6 +1836,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if value is None: value = type(None) if isinstance(value, str): + # class-level forward refs were handled above, this must be either + # a module-level annotation or a function argument annotation value = ForwardRef( value, is_argument=not isinstance(obj, types.ModuleType), From 3e033a8dd40e15755a1c79031c0a8761a65aa29a Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 24 Sep 2021 14:28:21 +0300 Subject: [PATCH 6/8] Update Lib/typing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index fd92f85d70ff4c..ada9adb0d32a58 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -667,7 +667,7 @@ class ForwardRef(_Final, _root=True): '__forward_is_argument__', '__forward_is_class__', '__forward_module__') - def __init__(self, arg, is_argument=True, is_class=False, module=None): + def __init__(self, arg, is_argument=True, module=None, *, is_class=False): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") try: From b66c8f65609f0928f701bee6bd2273cfa7a15126 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 24 Sep 2021 21:16:43 +0800 Subject: [PATCH 7/8] Update news entry --- .../next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst b/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst index 21f16d14b90420..b7242d45ea9be8 100644 --- a/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst +++ b/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst @@ -1,2 +1,2 @@ -Fixes that ``Final`` wrapped in ``ForwardRef`` was failing during -``get_type_hints``. +:func:`typing.get_type_hints` now works with :data:`~typing.Final` wrapped in +:class:`~typing.ForwardRef`. From 4f9e4c64faee00726dc564e4c964561ae04022b8 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 24 Sep 2021 21:18:24 +0800 Subject: [PATCH 8/8] Add comment/docstring to Lib/test/ann_module5.py --- Lib/test/ann_module5.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/ann_module5.py b/Lib/test/ann_module5.py index 1a413464b69896..837041e121f652 100644 --- a/Lib/test/ann_module5.py +++ b/Lib/test/ann_module5.py @@ -1,3 +1,5 @@ +# Used by test_typing to verify that Final wrapped in ForwardRef works. + from __future__ import annotations from typing import Final