Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Missing argument for ParamSpec

<!-- snapshot-diagnostics -->

For `ParamSpec` callables, both `*args` and `**kwargs` are required since the underlying callable's
signature is unknown. We add a sub-diagnostic explaining why these parameters are required.

```toml
[environment]
python-version = "3.12"
```

```py
from typing import Callable

def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
func() # error: [missing-argument]
func(*args) # error: [missing-argument]
func(**kwargs) # error: [missing-argument]
return wrapper
```
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,15 @@ def f[**P](func: Callable[P, int]) -> Callable[P, None]:
reveal_type(func(*kwargs, **args)) # revealed: int

# error: [invalid-argument-type] "Argument is incorrect: Expected `P@f.args`, found `P@f.kwargs`"
# error: [missing-argument]
reveal_type(func(args, kwargs)) # revealed: int

# Both parameters are required
# TODO: error
# error: [missing-argument]
reveal_type(func()) # revealed: int
# error: [missing-argument]
reveal_type(func(*args)) # revealed: int
# error: [missing-argument]
reveal_type(func(**kwargs)) # revealed: int
return wrapper
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: missing_argument_paramspec.md - Missing argument for ParamSpec
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Callable
2 |
3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
5 | func() # error: [missing-argument]
6 | func(*args) # error: [missing-argument]
7 | func(**kwargs) # error: [missing-argument]
8 | return wrapper
```

# Diagnostics

```
error[missing-argument]: No arguments provided for required parameters `*args`, `**kwargs`
--> src/mdtest_snippet.py:5:9
|
3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
5 | func() # error: [missing-argument]
| ^^^^^^
6 | func(*args) # error: [missing-argument]
7 | func(**kwargs) # error: [missing-argument]
|
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
info: rule `missing-argument` is enabled by default

```

```
error[missing-argument]: No argument provided for required parameter `**kwargs`
--> src/mdtest_snippet.py:6:9
|
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
5 | func() # error: [missing-argument]
6 | func(*args) # error: [missing-argument]
| ^^^^^^^^^^^
7 | func(**kwargs) # error: [missing-argument]
8 | return wrapper
|
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
info: rule `missing-argument` is enabled by default

```

```
error[missing-argument]: No argument provided for required parameter `*args`
--> src/mdtest_snippet.py:7:9
|
5 | func() # error: [missing-argument]
6 | func(*args) # error: [missing-argument]
7 | func(**kwargs) # error: [missing-argument]
| ^^^^^^^^^^^^^^
8 | return wrapper
|
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
info: rule `missing-argument` is enabled by default

```
29 changes: 26 additions & 3 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3013,6 +3013,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
});
}

// For ParamSpec parameters, both *args and **kwargs are required since we don't know
// what arguments the underlying callable expects. For all other callables, variadic
// and keyword_variadic parameters are optional.
let paramspec_parameters = self.parameters.as_paramspec().is_some();

let mut missing = vec![];
for (
index,
Expand All @@ -3027,11 +3032,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
continue;
}
let param = &self.parameters[index];
if param.is_variadic()
|| param.is_keyword_variadic()
if !paramspec_parameters && (param.is_variadic() || param.is_keyword_variadic())
|| param.default_type().is_some()
{
// variadic/keywords and defaulted arguments are not required
// (unless the parameters represent a ParamSpec)
continue;
}
missing.push(ParameterContext::new(param, index, false));
Expand All @@ -3040,6 +3045,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
if !missing.is_empty() {
self.errors.push(BindingError::MissingArguments {
parameters: ParameterContexts(missing),
paramspec: self.parameters.as_paramspec(),
});
}

Expand Down Expand Up @@ -4210,6 +4216,10 @@ pub(crate) enum BindingError<'db> {
/// One or more required parameters (that is, with no default) is not supplied by any argument.
MissingArguments {
parameters: ParameterContexts,
/// If the missing arguments are for a `ParamSpec`, this contains the `ParamSpec` typevar.
/// This is used to provide more informative error messages explaining why `*args` and
/// `**kwargs` are required.
paramspec: Option<BoundTypeVarInstance<'db>>,
},
/// A call argument can't be matched to any parameter.
UnknownArgument {
Expand Down Expand Up @@ -4549,7 +4559,10 @@ impl<'db> BindingError<'db> {
}
}

Self::MissingArguments { parameters } => {
Self::MissingArguments {
parameters,
paramspec,
} => {
if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) {
let s = if parameters.0.len() == 1 { "" } else { "s" };
let mut diag = builder.into_diagnostic(format_args!(
Expand All @@ -4576,6 +4589,16 @@ impl<'db> BindingError<'db> {
diag.sub(sub);
}
}
if let Some(paramspec) = paramspec {
let paramspec_name = paramspec.name(context.db());
diag.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format_args!(
"These arguments are required because `ParamSpec` `{paramspec_name}` \
could represent any set of parameters at runtime"
),
));
}
}
}

Expand Down