Skip to content

Commit 61b9b9c

Browse files
authored
[PEP646] Add tests/fixes for callable star args behavior (#15069)
This adds tests for mixing star args with typevar tuples from PEP646. In order to do this we have to handle an additional case in applytype where the result of expanding an unpack is a homogenous tuple rather than a list of types. This PR also includes (partially by accident) a fix to the empty case for type analysis. After re-reading the PEP we discover that the empty case is meant to be inferred as a Tuple[Any, ...] rather than an empty tuple and must fix that behavior as well as some of the previous test cases that assumed that it meant an empty tuple. When we actually call `target(*args)` from the testcase in `call` we expose a crash in mypy. This will be handled by a subsequent PR.
1 parent 8c14cba commit 61b9b9c

File tree

3 files changed

+97
-8
lines changed

3 files changed

+97
-8
lines changed

mypy/applytype.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mypy.types import (
99
AnyType,
1010
CallableType,
11+
Instance,
1112
Parameters,
1213
ParamSpecType,
1314
PartialType,
@@ -148,10 +149,20 @@ def apply_generic_arguments(
148149
assert False, f"mypy bug: unimplemented case, {expanded_tuple}"
149150
elif isinstance(unpacked_type, TypeVarTupleType):
150151
expanded_tvt = expand_unpack_with_variables(var_arg.typ, id_to_type)
151-
assert isinstance(expanded_tvt, list)
152-
for t in expanded_tvt:
153-
assert not isinstance(t, UnpackType)
154-
callable = replace_starargs(callable, expanded_tvt)
152+
if isinstance(expanded_tvt, list):
153+
for t in expanded_tvt:
154+
assert not isinstance(t, UnpackType)
155+
callable = replace_starargs(callable, expanded_tvt)
156+
else:
157+
assert isinstance(expanded_tvt, Instance)
158+
assert expanded_tvt.type.fullname == "builtins.tuple"
159+
callable = callable.copy_modified(
160+
arg_types=(
161+
callable.arg_types[:star_index]
162+
+ [expanded_tvt.args[0]]
163+
+ callable.arg_types[star_index + 1 :]
164+
)
165+
)
155166
else:
156167
assert False, "mypy bug: unhandled case applying unpack"
157168
else:

mypy/typeanal.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,8 +689,13 @@ def analyze_type_with_type_info(
689689
instance.args = tuple(self.pack_paramspec_args(instance.args))
690690

691691
if info.has_type_var_tuple_type:
692-
# - 1 to allow for the empty type var tuple case.
693-
valid_arg_length = len(instance.args) >= len(info.type_vars) - 1
692+
if instance.args:
693+
# -1 to account for empty tuple
694+
valid_arg_length = len(instance.args) >= len(info.type_vars) - 1
695+
# Empty case is special cased and we want to infer a Tuple[Any, ...]
696+
# instead of the empty tuple, so no - 1 here.
697+
else:
698+
valid_arg_length = False
694699
else:
695700
valid_arg_length = len(instance.args) == len(info.type_vars)
696701

@@ -1659,6 +1664,18 @@ def get_omitted_any(
16591664
return any_type
16601665

16611666

1667+
def fix_type_var_tuple_argument(any_type: Type, t: Instance) -> None:
1668+
if t.type.has_type_var_tuple_type:
1669+
args = list(t.args)
1670+
assert t.type.type_var_tuple_prefix is not None
1671+
tvt = t.type.defn.type_vars[t.type.type_var_tuple_prefix]
1672+
assert isinstance(tvt, TypeVarTupleType)
1673+
args[t.type.type_var_tuple_prefix] = UnpackType(
1674+
Instance(tvt.tuple_fallback.type, [any_type])
1675+
)
1676+
t.args = tuple(args)
1677+
1678+
16621679
def fix_instance(
16631680
t: Instance,
16641681
fail: MsgCallback,
@@ -1679,6 +1696,8 @@ def fix_instance(
16791696
fullname = t.type.fullname
16801697
any_type = get_omitted_any(disallow_any, fail, note, t, options, fullname, unexpanded_type)
16811698
t.args = (any_type,) * len(t.type.type_vars)
1699+
fix_type_var_tuple_argument(any_type, t)
1700+
16821701
return
16831702
# Invalid number of type parameters.
16841703
fail(
@@ -1690,6 +1709,7 @@ def fix_instance(
16901709
# otherwise the type checker may crash as it expects
16911710
# things to be right.
16921711
t.args = tuple(AnyType(TypeOfAny.from_error) for _ in t.type.type_vars)
1712+
fix_type_var_tuple_argument(AnyType(TypeOfAny.from_error), t)
16931713
t.invalid = True
16941714

16951715

test-data/unit/check-typevar-tuple.test

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ variadic_single: Variadic[int]
117117
reveal_type(variadic_single) # N: Revealed type is "__main__.Variadic[builtins.int]"
118118

119119
empty: Variadic[()]
120-
# TODO: fix pretty printer to be better.
121-
reveal_type(empty) # N: Revealed type is "__main__.Variadic"
120+
reveal_type(empty) # N: Revealed type is "__main__.Variadic[Unpack[builtins.tuple[Any, ...]]]"
122121

123122
bad: Variadic[Unpack[Tuple[int, ...]], str, Unpack[Tuple[bool, ...]]] # E: More than one Unpack in a type is not allowed
124123
reveal_type(bad) # N: Revealed type is "__main__.Variadic[Unpack[builtins.tuple[builtins.int, ...]], builtins.str]"
@@ -512,3 +511,62 @@ call_prefix(target=func_prefix, args=(0, 'foo'))
512511
call_prefix(target=func2_prefix, args=(0, 'foo')) # E: Argument "target" to "call_prefix" has incompatible type "Callable[[str, int, str], None]"; expected "Callable[[bytes, int, str], None]"
513512
[builtins fixtures/tuple.pyi]
514513

514+
[case testTypeVarTuplePep646UnspecifiedParameters]
515+
from typing import Tuple, Generic, TypeVar
516+
from typing_extensions import Unpack, TypeVarTuple
517+
518+
Ts = TypeVarTuple("Ts")
519+
520+
class Array(Generic[Unpack[Ts]]):
521+
...
522+
523+
def takes_any_array(arr: Array) -> None:
524+
...
525+
526+
x: Array[int, bool]
527+
takes_any_array(x)
528+
529+
T = TypeVar("T")
530+
531+
class Array2(Generic[T, Unpack[Ts]]):
532+
...
533+
534+
def takes_empty_array2(arr: Array2[int]) -> None:
535+
...
536+
537+
y: Array2[int]
538+
takes_empty_array2(y)
539+
[builtins fixtures/tuple.pyi]
540+
541+
[case testTypeVarTuplePep646CallableStarArgs]
542+
from typing import Tuple, Callable
543+
from typing_extensions import Unpack, TypeVarTuple
544+
545+
Ts = TypeVarTuple("Ts")
546+
547+
def call(
548+
target: Callable[[Unpack[Ts]], None],
549+
*args: Unpack[Ts],
550+
) -> None:
551+
...
552+
# TODO: exposes unhandled case in checkexpr
553+
# target(*args)
554+
555+
class A:
556+
def func(self, arg1: int, arg2: str) -> None: ...
557+
def func2(self, arg1: int, arg2: int) -> None: ...
558+
def func3(self, *args: int) -> None: ...
559+
560+
vargs: Tuple[int, ...]
561+
vargs_str: Tuple[str, ...]
562+
563+
call(A().func) # E: Argument 1 to "call" has incompatible type "Callable[[int, str], None]"; expected "Callable[[VarArg(object)], None]"
564+
call(A().func, 0, 'foo')
565+
call(A().func, 0, 'foo', 0) # E: Argument 1 to "call" has incompatible type "Callable[[int, str], None]"; expected "Callable[[VarArg(object)], None]"
566+
call(A().func, 0) # E: Argument 1 to "call" has incompatible type "Callable[[int, str], None]"; expected "Callable[[VarArg(object)], None]"
567+
call(A().func, 0, 1) # E: Argument 1 to "call" has incompatible type "Callable[[int, str], None]"; expected "Callable[[int, object], None]"
568+
call(A().func2, 0, 0)
569+
call(A().func3, 0, 1, 2)
570+
call(A().func3)
571+
572+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)