From 3bb397511b74d3d7411a96650d4b5d2c674c5690 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 14 Jan 2023 12:59:25 +0300 Subject: [PATCH 1/6] gh-101015: Fix `typing.ForwardRef` with `*tuple` unpack --- Lib/test/test_typing.py | 16 ++++++++++++++++ Lib/typing.py | 12 ++++++++++-- ...023-01-14-12-58-21.gh-issue-101015.stWFid.rst | 2 ++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 1cae1b0de7140f..f5f89e8a679491 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1224,6 +1224,22 @@ class D(Generic[Unpack[Ts]]): pass self.assertIs(D[T].__origin__, D) self.assertIs(D[Unpack[Ts]].__origin__, D) + def test_get_type_hints_on_unpack_args(self): + Ts = TypeVarTuple('Ts') + + def func1(*args: '*Ts'): pass + self.assertEqual(gth(func1, localns={'Ts': Ts}), + {'args': Unpack[Ts]}) + + def func2(*args: '*tuple[int, str]'): pass + self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + + class CustomVariadic(Generic[*Ts]): pass + + def func3(*args: '*CustomVariadic[int, str]'): pass + self.assertEqual(gth(func3, localns={'CustomVariadic': CustomVariadic}), + {'args': Unpack[CustomVariadic[int, str]]}) + def test_tuple_args_are_correct(self): Ts = TypeVarTuple('Ts') diff --git a/Lib/typing.py b/Lib/typing.py index 4675af12d087b6..95db4d7b9aa85a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -818,7 +818,7 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', '__forward_is_argument__', '__forward_is_class__', - '__forward_module__') + '__forward_module__', '__forward_is_unpack__') def __init__(self, arg, is_argument=True, module=None, *, is_class=False): if not isinstance(arg, str): @@ -828,9 +828,11 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): # Unfortunately, this isn't a valid expression on its own, so we # do the unpacking manually. if arg[0] == '*': - arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] + arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (tuple[int, int],)[0] + is_unpack = True else: arg_to_compile = arg + is_unpack = False try: code = compile(arg_to_compile, '', 'eval') except SyntaxError: @@ -843,6 +845,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): self.__forward_is_argument__ = is_argument self.__forward_is_class__ = is_class self.__forward_module__ = module + self.__forward_is_unpack__ = is_unpack def _evaluate(self, globalns, localns, recursive_guard): if self.__forward_arg__ in recursive_guard: @@ -867,6 +870,11 @@ def _evaluate(self, globalns, localns, recursive_guard): self.__forward_value__ = _eval_type( type_, globalns, localns, recursive_guard | {self.__forward_arg__} ) + if ( + self.__forward_is_unpack__ and + not isinstance(self.__forward_value__, _UnpackGenericAlias) + ): + self.__forward_value__ = Unpack[self.__forward_value__] self.__forward_evaluated__ = True return self.__forward_value__ diff --git a/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst b/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst new file mode 100644 index 00000000000000..ca2736a4b3da94 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst @@ -0,0 +1,2 @@ +Fix :class:`typing.ForwardRef` evaluation of ``'*tuple[...]'`` string. It +must not drop the ``Unpack`` part. From 1d2781ed468b37f004435ce17f98dc31dacffef5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 14 Jan 2023 13:38:26 +0300 Subject: [PATCH 2/6] Fix tests in `test_future` --- Lib/test/test_future.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index b8b591a1bcf2c6..41ebf9c95046e9 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -419,6 +419,8 @@ def bar(arg: (yield)): pass """)) def test_get_type_hints_on_func_with_variadic_arg(self): + import typing + # `typing.get_type_hints` might break on a function with a variadic # annotation (e.g. `f(*args: *Ts)`) if `from __future__ import # annotations`, because it could try to evaluate `*Ts` as an expression, @@ -435,7 +437,8 @@ def f(*args: *c): pass """)) hints = namespace.pop('hints') - self.assertIsInstance(hints['args'], namespace['StarredC']) + self.assertIsInstance(hints['args'], + typing.Unpack[namespace['StarredC']]) if __name__ == "__main__": From d774d420ae123f7a3929cfbe5157216a30b9e522 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 14 Jan 2023 15:38:14 +0300 Subject: [PATCH 3/6] Also test unstringified unpack annotations --- Lib/test/test_future.py | 4 ++-- Lib/test/test_typing.py | 14 ++++++++++++++ Lib/typing.py | 3 +++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index 41ebf9c95046e9..4664249cf920e2 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -437,8 +437,8 @@ def f(*args: *c): pass """)) hints = namespace.pop('hints') - self.assertIsInstance(hints['args'], - typing.Unpack[namespace['StarredC']]) + self.assertEqual(hints['args'].__origin__, typing.Unpack) + self.assertIsInstance(hints['args'].__args__[0], namespace['StarredC']) if __name__ == "__main__": diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f5f89e8a679491..5aa49bb0e2456d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1227,6 +1227,20 @@ class D(Generic[Unpack[Ts]]): pass def test_get_type_hints_on_unpack_args(self): Ts = TypeVarTuple('Ts') + def func1(*args: *Ts): pass + self.assertEqual(gth(func1), {'args': Unpack[Ts]}) + + def func2(*args: *tuple[int, str]): pass + self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + + class CustomVariadic(Generic[*Ts]): pass + + def func3(*args: *CustomVariadic[int, str]): pass + self.assertEqual(gth(func3), {'args': Unpack[CustomVariadic[int, str]]}) + + def test_get_type_hints_on_unpack_args_string(self): + Ts = TypeVarTuple('Ts') + def func1(*args: '*Ts'): pass self.assertEqual(gth(func1, localns={'Ts': Ts}), {'args': Unpack[Ts]}) diff --git a/Lib/typing.py b/Lib/typing.py index 95db4d7b9aa85a..4db20b67fb170f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -371,10 +371,13 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): ForwardRef(arg) if isinstance(arg, str) else arg for arg in t.__args__ ) + is_unpacked = t.__unpacked__ if _should_unflatten_callable_args(t, args): t = t.__origin__[(args[:-1], args[-1])] else: t = t.__origin__[args] + if is_unpacked: + t = Unpack[t] ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) if ev_args == t.__args__: return t From 1f2ffdb6a762ac2feabe553b21cab805e73954b2 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 14 Jan 2023 18:55:06 +0300 Subject: [PATCH 4/6] Simplify code --- Lib/test/test_future.py | 3 +-- Lib/typing.py | 12 ++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index 4664249cf920e2..7575276aa707bb 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -437,8 +437,7 @@ def f(*args: *c): pass """)) hints = namespace.pop('hints') - self.assertEqual(hints['args'].__origin__, typing.Unpack) - self.assertIsInstance(hints['args'].__args__[0], namespace['StarredC']) + self.assertIsInstance(hints['args'], namespace['StarredC']) if __name__ == "__main__": diff --git a/Lib/typing.py b/Lib/typing.py index 4db20b67fb170f..bdf51bb5f41595 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -821,7 +821,7 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', '__forward_is_argument__', '__forward_is_class__', - '__forward_module__', '__forward_is_unpack__') + '__forward_module__') def __init__(self, arg, is_argument=True, module=None, *, is_class=False): if not isinstance(arg, str): @@ -831,11 +831,9 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): # Unfortunately, this isn't a valid expression on its own, so we # do the unpacking manually. if arg[0] == '*': - arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (tuple[int, int],)[0] - is_unpack = True + arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] else: arg_to_compile = arg - is_unpack = False try: code = compile(arg_to_compile, '', 'eval') except SyntaxError: @@ -848,7 +846,6 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): self.__forward_is_argument__ = is_argument self.__forward_is_class__ = is_class self.__forward_module__ = module - self.__forward_is_unpack__ = is_unpack def _evaluate(self, globalns, localns, recursive_guard): if self.__forward_arg__ in recursive_guard: @@ -873,11 +870,6 @@ def _evaluate(self, globalns, localns, recursive_guard): self.__forward_value__ = _eval_type( type_, globalns, localns, recursive_guard | {self.__forward_arg__} ) - if ( - self.__forward_is_unpack__ and - not isinstance(self.__forward_value__, _UnpackGenericAlias) - ): - self.__forward_value__ = Unpack[self.__forward_value__] self.__forward_evaluated__ = True return self.__forward_value__ From 2857380d0b7f33ce65231a7d307f54c02bdaa792 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 14 Jan 2023 18:55:24 +0300 Subject: [PATCH 5/6] Simplify code --- Lib/test/test_future.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index 7575276aa707bb..b8b591a1bcf2c6 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -419,8 +419,6 @@ def bar(arg: (yield)): pass """)) def test_get_type_hints_on_func_with_variadic_arg(self): - import typing - # `typing.get_type_hints` might break on a function with a variadic # annotation (e.g. `f(*args: *Ts)`) if `from __future__ import # annotations`, because it could try to evaluate `*Ts` as an expression, From f7dda808093b6ea36b2dc60c40e3fe508e5f1868 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 23 Jan 2023 09:59:58 +0300 Subject: [PATCH 6/6] Update 2023-01-14-12-58-21.gh-issue-101015.stWFid.rst --- .../Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst b/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst index ca2736a4b3da94..b9d73ff9855236 100644 --- a/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst +++ b/Misc/NEWS.d/next/Library/2023-01-14-12-58-21.gh-issue-101015.stWFid.rst @@ -1,2 +1,2 @@ -Fix :class:`typing.ForwardRef` evaluation of ``'*tuple[...]'`` string. It -must not drop the ``Unpack`` part. +Fix :func:`typing.get_type_hints` on ``'*tuple[...]'`` and ``*tuple[...]``. +It must not drop the ``Unpack`` part.