From 784905dbeff68cf788bbeefe0a675af1af04affc Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 25 Sep 2021 11:56:22 +0300 Subject: [PATCH] bpo-45166: fixes `get_type_hints` failure on `Final` (GH-28279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Langa Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> --- Lib/test/ann_module5.py | 10 +++++++ Lib/test/ann_module6.py | 7 +++++ Lib/test/test_typing.py | 18 +++++++++++- Lib/typing.py | 29 +++++++++++++------ .../2021-09-10-21-35-53.bpo-45166.UHipXF.rst | 2 ++ 5 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 Lib/test/ann_module5.py create mode 100644 Lib/test/ann_module6.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 00000000000..837041e121f --- /dev/null +++ b/Lib/test/ann_module5.py @@ -0,0 +1,10 @@ +# Used by test_typing to verify that Final wrapped in ForwardRef works. + +from __future__ import annotations + +from typing import Final + +name: Final[str] = "final" + +class MyClass: + value: Final = 3000 diff --git a/Lib/test/ann_module6.py b/Lib/test/ann_module6.py new file mode 100644 index 00000000000..679175669bc --- /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 fa49b90886c..d1887f7eee4 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, ann_module6 from typing import AsyncContextManager class A: @@ -3339,6 +3339,22 @@ 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}) + + 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 e29d699283d..ada9adb0d32 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, 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 @@ -164,14 +164,16 @@ 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: - invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) + 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 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") @@ -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, module=None, *, is_class=False): 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()} @@ -1831,7 +1836,13 @@ 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) + # 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), + is_class=False, + ) 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 00000000000..b7242d45ea9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-10-21-35-53.bpo-45166.UHipXF.rst @@ -0,0 +1,2 @@ +:func:`typing.get_type_hints` now works with :data:`~typing.Final` wrapped in +:class:`~typing.ForwardRef`.