Skip to content

Commit 9518b6a

Browse files
authored
Reject ParamSpec-typed callables calls with insufficient arguments (#17323)
Fixes #14571. When type checking a call of a `ParamSpec`-typed callable, currently there is an incorrect "fast path" (if there are two arguments of shape `(*args: P.args, **kwargs: P.kwargs)`, accept), which breaks with `Concatenate` (such call was accepted even for `Concatenate[int, P]`). Also there was no checking that args and kwargs are actually present: since `*args` and `**kwargs` are not required, their absence was silently accepted.
1 parent cf3db99 commit 9518b6a

File tree

2 files changed

+128
-6
lines changed

2 files changed

+128
-6
lines changed

mypy/checkexpr.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -1756,7 +1756,11 @@ def check_callable_call(
17561756
)
17571757

17581758
param_spec = callee.param_spec()
1759-
if param_spec is not None and arg_kinds == [ARG_STAR, ARG_STAR2]:
1759+
if (
1760+
param_spec is not None
1761+
and arg_kinds == [ARG_STAR, ARG_STAR2]
1762+
and len(formal_to_actual) == 2
1763+
):
17601764
arg1 = self.accept(args[0])
17611765
arg2 = self.accept(args[1])
17621766
if (
@@ -2362,6 +2366,9 @@ def check_argument_count(
23622366
# Positional argument when expecting a keyword argument.
23632367
self.msg.too_many_positional_arguments(callee, context)
23642368
ok = False
2369+
elif callee.param_spec() is not None and not formal_to_actual[i]:
2370+
self.msg.too_few_arguments(callee, context, actual_names)
2371+
ok = False
23652372
return ok
23662373

23672374
def check_for_extra_actual_arguments(
@@ -2763,9 +2770,9 @@ def plausible_overload_call_targets(
27632770
) -> list[CallableType]:
27642771
"""Returns all overload call targets that having matching argument counts.
27652772
2766-
If the given args contains a star-arg (*arg or **kwarg argument), this method
2767-
will ensure all star-arg overloads appear at the start of the list, instead
2768-
of their usual location.
2773+
If the given args contains a star-arg (*arg or **kwarg argument, including
2774+
ParamSpec), this method will ensure all star-arg overloads appear at the start
2775+
of the list, instead of their usual location.
27692776
27702777
The only exception is if the starred argument is something like a Tuple or a
27712778
NamedTuple, which has a definitive "shape". If so, we don't move the corresponding
@@ -2793,9 +2800,13 @@ def has_shape(typ: Type) -> bool:
27932800
formal_to_actual = map_actuals_to_formals(
27942801
arg_kinds, arg_names, typ.arg_kinds, typ.arg_names, lambda i: arg_types[i]
27952802
)
2796-
27972803
with self.msg.filter_errors():
2798-
if self.check_argument_count(
2804+
if typ.param_spec() is not None:
2805+
# ParamSpec can be expanded in a lot of different ways. We may try
2806+
# to expand it here instead, but picking an impossible overload
2807+
# is safe: it will be filtered out later.
2808+
star_matches.append(typ)
2809+
elif self.check_argument_count(
27992810
typ, arg_types, arg_kinds, arg_names, formal_to_actual, None
28002811
):
28012812
if args_have_var_arg and typ.is_var_arg:

test-data/unit/check-parameter-specification.test

+111
Original file line numberDiff line numberDiff line change
@@ -2192,3 +2192,114 @@ parametrize(_test, Case(1, b=2), Case(3, b=4))
21922192
parametrize(_test, Case(1, 2), Case(3))
21932193
parametrize(_test, Case(1, 2), Case(3, b=4))
21942194
[builtins fixtures/paramspec.pyi]
2195+
2196+
[case testRunParamSpecInsufficientArgs]
2197+
from typing_extensions import ParamSpec, Concatenate
2198+
from typing import Callable
2199+
2200+
_P = ParamSpec("_P")
2201+
2202+
def run(predicate: Callable[_P, None], *args: _P.args, **kwargs: _P.kwargs) -> None: # N: "run" defined here
2203+
predicate() # E: Too few arguments
2204+
predicate(*args) # E: Too few arguments
2205+
predicate(**kwargs) # E: Too few arguments
2206+
predicate(*args, **kwargs)
2207+
2208+
def fn() -> None: ...
2209+
def fn_args(x: int) -> None: ...
2210+
def fn_posonly(x: int, /) -> None: ...
2211+
2212+
run(fn)
2213+
run(fn_args, 1)
2214+
run(fn_args, x=1)
2215+
run(fn_posonly, 1)
2216+
run(fn_posonly, x=1) # E: Unexpected keyword argument "x" for "run"
2217+
2218+
[builtins fixtures/paramspec.pyi]
2219+
2220+
[case testRunParamSpecConcatenateInsufficientArgs]
2221+
from typing_extensions import ParamSpec, Concatenate
2222+
from typing import Callable
2223+
2224+
_P = ParamSpec("_P")
2225+
2226+
def run(predicate: Callable[Concatenate[int, _P], None], *args: _P.args, **kwargs: _P.kwargs) -> None: # N: "run" defined here
2227+
predicate() # E: Too few arguments
2228+
predicate(1) # E: Too few arguments
2229+
predicate(1, *args) # E: Too few arguments
2230+
predicate(1, *args) # E: Too few arguments
2231+
predicate(1, **kwargs) # E: Too few arguments
2232+
predicate(*args, **kwargs) # E: Argument 1 has incompatible type "*_P.args"; expected "int"
2233+
predicate(1, *args, **kwargs)
2234+
2235+
def fn() -> None: ...
2236+
def fn_args(x: int, y: str) -> None: ...
2237+
def fn_posonly(x: int, /) -> None: ...
2238+
def fn_posonly_args(x: int, /, y: str) -> None: ...
2239+
2240+
run(fn) # E: Argument 1 to "run" has incompatible type "Callable[[], None]"; expected "Callable[[int], None]"
2241+
run(fn_args, 1, 'a') # E: Too many arguments for "run" \
2242+
# E: Argument 2 to "run" has incompatible type "int"; expected "str"
2243+
run(fn_args, y='a')
2244+
run(fn_args, 'a')
2245+
run(fn_posonly)
2246+
run(fn_posonly, x=1) # E: Unexpected keyword argument "x" for "run"
2247+
run(fn_posonly_args) # E: Missing positional argument "y" in call to "run"
2248+
run(fn_posonly_args, 'a')
2249+
run(fn_posonly_args, y='a')
2250+
2251+
[builtins fixtures/paramspec.pyi]
2252+
2253+
[case testRunParamSpecConcatenateInsufficientArgsInDecorator]
2254+
from typing_extensions import ParamSpec, Concatenate
2255+
from typing import Callable
2256+
2257+
P = ParamSpec("P")
2258+
2259+
def decorator(fn: Callable[Concatenate[str, P], None]) -> Callable[P, None]:
2260+
def inner(*args: P.args, **kwargs: P.kwargs) -> None:
2261+
fn("value") # E: Too few arguments
2262+
fn("value", *args) # E: Too few arguments
2263+
fn("value", **kwargs) # E: Too few arguments
2264+
fn(*args, **kwargs) # E: Argument 1 has incompatible type "*P.args"; expected "str"
2265+
fn("value", *args, **kwargs)
2266+
return inner
2267+
2268+
@decorator
2269+
def foo(s: str, s2: str) -> None: ...
2270+
2271+
[builtins fixtures/paramspec.pyi]
2272+
2273+
[case testRunParamSpecOverload]
2274+
from typing_extensions import ParamSpec
2275+
from typing import Callable, NoReturn, TypeVar, Union, overload
2276+
2277+
P = ParamSpec("P")
2278+
T = TypeVar("T")
2279+
2280+
@overload
2281+
def capture(
2282+
sync_fn: Callable[P, NoReturn],
2283+
*args: P.args,
2284+
**kwargs: P.kwargs,
2285+
) -> int: ...
2286+
@overload
2287+
def capture(
2288+
sync_fn: Callable[P, T],
2289+
*args: P.args,
2290+
**kwargs: P.kwargs,
2291+
) -> Union[T, int]: ...
2292+
def capture(
2293+
sync_fn: Callable[P, T],
2294+
*args: P.args,
2295+
**kwargs: P.kwargs,
2296+
) -> Union[T, int]:
2297+
return sync_fn(*args, **kwargs)
2298+
2299+
def fn() -> str: return ''
2300+
def err() -> NoReturn: ...
2301+
2302+
reveal_type(capture(fn)) # N: Revealed type is "Union[builtins.str, builtins.int]"
2303+
reveal_type(capture(err)) # N: Revealed type is "builtins.int"
2304+
2305+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)