Skip to content

Commit 8e11117

Browse files
committed
gh-101015: Fix typing.ForwardRef with *tuple unpack
1 parent ef633e5 commit 8e11117

File tree

3 files changed

+30
-8
lines changed

3 files changed

+30
-8
lines changed

Lib/test/test_typing.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,22 @@ class D(Generic[Unpack[Ts]]): pass
12241224
self.assertIs(D[T].__origin__, D)
12251225
self.assertIs(D[Unpack[Ts]].__origin__, D)
12261226

1227+
def test_get_type_hints_on_unpack_args(self):
1228+
Ts = TypeVarTuple('Ts')
1229+
1230+
def func1(*args: '*Ts'): pass
1231+
self.assertEqual(gth(func1, localns={'Ts': Ts}),
1232+
{'args': Unpack[Ts]})
1233+
1234+
def func2(*args: '*tuple[int, str]'): pass
1235+
self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]})
1236+
1237+
class CustomVariadic(Generic[*Ts]): pass
1238+
1239+
def func3(*args: '*CustomVariadic[int, str]'): pass
1240+
self.assertEqual(gth(func3, localns={'CustomVariadic': CustomVariadic}),
1241+
{'args': Unpack[CustomVariadic[int, str]]})
1242+
12271243
def test_tuple_args_are_correct(self):
12281244
Ts = TypeVarTuple('Ts')
12291245

Lib/typing.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ class ForwardRef(_Final, _root=True):
818818
__slots__ = ('__forward_arg__', '__forward_code__',
819819
'__forward_evaluated__', '__forward_value__',
820820
'__forward_is_argument__', '__forward_is_class__',
821-
'__forward_module__')
821+
'__forward_module__', '__forward_is_unpack__')
822822

823823
def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
824824
if not isinstance(arg, str):
@@ -828,9 +828,11 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
828828
# Unfortunately, this isn't a valid expression on its own, so we
829829
# do the unpacking manually.
830830
if arg[0] == '*':
831-
arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0]
831+
arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (tuple[int, int],)[0]
832+
is_unpack = True
832833
else:
833834
arg_to_compile = arg
835+
is_unpack = False
834836
try:
835837
code = compile(arg_to_compile, '<string>', 'eval')
836838
except SyntaxError:
@@ -843,6 +845,7 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
843845
self.__forward_is_argument__ = is_argument
844846
self.__forward_is_class__ = is_class
845847
self.__forward_module__ = module
848+
self.__forward_is_unpack__ = is_unpack
846849

847850
def _evaluate(self, globalns, localns, recursive_guard):
848851
if self.__forward_arg__ in recursive_guard:
@@ -867,6 +870,11 @@ def _evaluate(self, globalns, localns, recursive_guard):
867870
self.__forward_value__ = _eval_type(
868871
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
869872
)
873+
if (
874+
self.__forward_is_unpack__ and
875+
not isinstance(self.__forward_value__, _UnpackGenericAlias)
876+
):
877+
self.__forward_value__ = Unpack[self.__forward_value__]
870878
self.__forward_evaluated__ = True
871879
return self.__forward_value__
872880

@@ -1939,15 +1947,11 @@ def _no_init_or_replace_init(self, *args, **kwargs):
19391947

19401948

19411949
def _caller(depth=1, default='__main__'):
1942-
try:
1943-
return sys._getframemodulename(depth + 1) or default
1944-
except AttributeError: # For platforms without _getframemodulename()
1945-
pass
19461950
try:
19471951
return sys._getframe(depth + 1).f_globals.get('__name__', default)
19481952
except (AttributeError, ValueError): # For platforms without _getframe()
1949-
pass
1950-
return None
1953+
return None
1954+
19511955

19521956
def _allow_reckless_class_checks(depth=3):
19531957
"""Allow instance and class checks for special stdlib modules.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`typing.ForwardRef` evaluation of ``'*tuple[...]'`` string. It
2+
must not drop the ``Unpack`` part.

0 commit comments

Comments
 (0)