From 1a310300402bd9f402504c25cec6866d39f4abe3 Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Wed, 26 Jan 2022 11:52:20 -0800 Subject: [PATCH 1/6] Pass status of special forms to forward references Previously this didn't matter because there weren't any valid code paths that could trigger a type check with a special form, but after the bug fix for `Annotated` wrapping special forms it's now possible to annotate something like `Annotated['ClassVar[int]', (3, 4)]`. This change would also be needed for proposed future changes, such as allowing `ClassVar` and `Final` to nest each other in dataclasses. --- Lib/test/test_typing.py | 7 +++++++ Lib/typing.py | 18 +++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b5767d02691d8d..fe012dbe7f4add 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2870,6 +2870,13 @@ def foo(a: 'Callable[..., T]'): self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': Callable[..., T]}) + def test_special_forms_forward(self): + + class C: + a: Annotated['ClassVar[int]', (3, 5)] = 4 + + self.assertEqual(get_type_hints(C, globals())['a'], ClassVar[int]) + def test_syntax_error(self): with self.assertRaises(SyntaxError): diff --git a/Lib/typing.py b/Lib/typing.py index e3e098b1fcc8f3..98f34161ee0720 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -142,12 +142,12 @@ def _idfunc(_, x): # legitimate imports of those modules. -def _type_convert(arg, module=None): +def _type_convert(arg, module=None, *, allow_special_forms=False): """For converting None to type(None), and strings to ForwardRef.""" if arg is None: return type(None) if isinstance(arg, str): - return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, allow_special_forms=allow_special_forms) return arg @@ -169,7 +169,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms= if is_argument: invalid_generic_forms += (Final,) - arg = _type_convert(arg, module=module) + arg = _type_convert(arg, module=module, allow_special_forms=allow_special_forms) if (isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms): raise TypeError(f"{arg} is not valid as type argument") @@ -661,10 +661,10 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__', '__forward_is_class__', + '__forward_is_argument__', '__forward_allow_special_forms__', '__forward_module__') - def __init__(self, arg, is_argument=True, module=None, *, is_class=False): + def __init__(self, arg, is_argument=True, module=None, *, allow_special_forms=False): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") try: @@ -676,7 +676,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument - self.__forward_is_class__ = is_class + self.__forward_allow_special_forms__ = allow_special_forms self.__forward_module__ = module def _evaluate(self, globalns, localns, recursive_guard): @@ -697,7 +697,7 @@ def _evaluate(self, globalns, localns, recursive_guard): eval(self.__forward_code__, globalns, localns), "Forward references must evaluate to types.", is_argument=self.__forward_is_argument__, - allow_special_forms=self.__forward_is_class__, + allow_special_forms=self.__forward_allow_special_forms__, ) self.__forward_value__ = _eval_type( type_, globalns, localns, recursive_guard | {self.__forward_arg__} @@ -1804,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, is_class=True) + value = ForwardRef(value, is_argument=False, allow_special_forms=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()} @@ -1841,7 +1841,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = ForwardRef( value, is_argument=not isinstance(obj, types.ModuleType), - is_class=False, + allow_special_forms=False, ) value = _eval_type(value, globalns, localns) if name in defaults and defaults[name] is None: From 1b4c262cddeb3c21fa7585169f7a8c4493ebea74 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 26 Jan 2022 20:36:31 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst diff --git a/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst new file mode 100644 index 00000000000000..e58531faad8b3f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst @@ -0,0 +1 @@ +typing: Pass on status of special forms to forward references. Patch by Gregory Beauregard. \ No newline at end of file From 535a124945fc871168573d0314c92796d4fe69a3 Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Wed, 26 Jan 2022 13:41:23 -0800 Subject: [PATCH 3/6] restore ForwardRef public api arg name --- Lib/typing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 98f34161ee0720..5c1d42038995b8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -147,7 +147,7 @@ def _type_convert(arg, module=None, *, allow_special_forms=False): if arg is None: return type(None) if isinstance(arg, str): - return ForwardRef(arg, module=module, allow_special_forms=allow_special_forms) + return ForwardRef(arg, module=module, is_class=allow_special_forms) return arg @@ -664,7 +664,7 @@ class ForwardRef(_Final, _root=True): '__forward_is_argument__', '__forward_allow_special_forms__', '__forward_module__') - def __init__(self, arg, is_argument=True, module=None, *, allow_special_forms=False): + 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,7 +676,7 @@ def __init__(self, arg, is_argument=True, module=None, *, allow_special_forms=Fa self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument - self.__forward_allow_special_forms__ = allow_special_forms + self.__forward_allow_special_forms__ = is_class self.__forward_module__ = module def _evaluate(self, globalns, localns, recursive_guard): @@ -1804,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, allow_special_forms=True) + 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()} @@ -1841,7 +1841,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = ForwardRef( value, is_argument=not isinstance(obj, types.ModuleType), - allow_special_forms=False, + is_class=False, ) value = _eval_type(value, globalns, localns) if name in defaults and defaults[name] is None: From 32deeb8c2983311dc010b25ea95133c7891bd222 Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Wed, 26 Jan 2022 17:23:28 -0800 Subject: [PATCH 4/6] update NEWS and change dunder method back --- Lib/typing.py | 6 +++--- .../next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 5c1d42038995b8..450cd7b51184ef 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -661,7 +661,7 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__', '__forward_allow_special_forms__', + '__forward_is_argument__', '__forward_is_class__', '__forward_module__') def __init__(self, arg, is_argument=True, module=None, *, is_class=False): @@ -676,7 +676,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument - self.__forward_allow_special_forms__ = is_class + self.__forward_is_class__ = is_class self.__forward_module__ = module def _evaluate(self, globalns, localns, recursive_guard): @@ -697,7 +697,7 @@ def _evaluate(self, globalns, localns, recursive_guard): eval(self.__forward_code__, globalns, localns), "Forward references must evaluate to types.", is_argument=self.__forward_is_argument__, - allow_special_forms=self.__forward_allow_special_forms__, + allow_special_forms=self.__forward_is_class__, ) self.__forward_value__ = _eval_type( type_, globalns, localns, recursive_guard | {self.__forward_arg__} diff --git a/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst index e58531faad8b3f..2dc2808ac83ebf 100644 --- a/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst +++ b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst @@ -1 +1 @@ -typing: Pass on status of special forms to forward references. Patch by Gregory Beauregard. \ No newline at end of file +In :func:`typing.get_type_hints`, support evaluating stringified `ClassVar` and `Final` annotations inside `Annotated`. Patch by Gregory Beauregard. From 41aafb4e35bc8c60336f211b7f4b4ef6a9e69301 Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Wed, 26 Jan 2022 17:30:16 -0800 Subject: [PATCH 5/6] add more tests to further cover behavior --- Lib/test/test_typing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fe012dbe7f4add..4b260d49bdfe46 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2874,8 +2874,15 @@ def test_special_forms_forward(self): class C: a: Annotated['ClassVar[int]', (3, 5)] = 4 + b: Annotated['Final[int]', "const"] = 4 + + class CF: + b: List['Final[int]'] = 4 self.assertEqual(get_type_hints(C, globals())['a'], ClassVar[int]) + self.assertEqual(get_type_hints(C, globals())['b'], Final[int]) + with self.assertRaises(TypeError): + get_type_hints(CF, globals()), def test_syntax_error(self): From 8c148e246482e9cb87407f8a52d32f41a97e070f Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Wed, 26 Jan 2022 17:36:36 -0800 Subject: [PATCH 6/6] use double backticks for RST --- .../next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst index 2dc2808ac83ebf..2bdde21b6e58e5 100644 --- a/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst +++ b/Misc/NEWS.d/next/Library/2022-01-26-20-36-30.bpo-46539.23iW1d.rst @@ -1 +1 @@ -In :func:`typing.get_type_hints`, support evaluating stringified `ClassVar` and `Final` annotations inside `Annotated`. Patch by Gregory Beauregard. +In :func:`typing.get_type_hints`, support evaluating stringified ``ClassVar`` and ``Final`` annotations inside ``Annotated``. Patch by Gregory Beauregard.