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
Expand Up @@ -858,6 +858,52 @@ static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic]))
```

But, there are special cases when matching against standard parameters. This is due to the fact that
a standard parameter can be passed as a positional or keyword parameter. This means that the
subtyping relation needs to consider both cases.

```py
def variadic_keyword(*args: int, **kwargs: int) -> None: ...
def standard_int(a: int) -> None: ...
def standard_float(a: float) -> None: ...

static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int]))
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float]))
```

If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard
parameter, then the subtyping relation is invalid.

```py
def variadic_bool(*args: bool, **kwargs: int) -> None: ...
def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ...

static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int]))
static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int]))
```

The standard parameter can follow a variadic parameter in the subtype.

```py
def standard_variadic_int(a: int, *args: int) -> None: ...
def standard_variadic_float(a: int, *args: float) -> None: ...

static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int]))
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float]))
```

The keyword part of the standard parameter can be matched against keyword-only parameter with the
same name if the keyword-variadic parameter is absent.

```py
def variadic_a(*args: int, a: int) -> None: ...
def variadic_b(*args: int, b: int) -> None: ...

static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int]))
# The parameter name is different
static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int]))
```

#### Keyword-only

For keyword-only parameters, the name should be the same:
Expand Down
48 changes: 39 additions & 9 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4569,6 +4569,10 @@ impl<'db> GeneralCallableType<'db> {
iter_other: other_signature.parameters().iter(),
};

// Collect all the standard parameters that have only been matched against a variadic
// parameter which means that the keyword variant is still unmatched.
let mut other_keywords = Vec::new();

loop {
let Some(next_parameter) = parameters.next() else {
// All parameters have been checked or both the parameter lists were empty. In
Expand All @@ -4578,6 +4582,14 @@ impl<'db> GeneralCallableType<'db> {

match next_parameter {
EitherOrBoth::Left(self_parameter) => match self_parameter.kind() {
ParameterKind::KeywordOnly { .. } | ParameterKind::KeywordVariadic { .. }
if !other_keywords.is_empty() =>
{
// If there are any unmatched keyword parameters in `other`, they need to
// be checked against the keyword-only / keyword-variadic parameters that
// will be done after this loop.
break;
}
ParameterKind::PositionalOnly { default_type, .. }
| ParameterKind::PositionalOrKeyword { default_type, .. }
| ParameterKind::KeywordOnly { default_type, .. } => {
Expand Down Expand Up @@ -4652,14 +4664,25 @@ impl<'db> GeneralCallableType<'db> {
}
}

(ParameterKind::Variadic { .. }, ParameterKind::PositionalOnly { .. }) => {
(
ParameterKind::Variadic { .. },
ParameterKind::PositionalOnly { .. }
| ParameterKind::PositionalOrKeyword { .. },
) => {
if !check_types(
other_parameter.annotated_type(),
self_parameter.annotated_type(),
) {
return false;
}

if matches!(
other_parameter.kind(),
ParameterKind::PositionalOrKeyword { .. }
) {
other_keywords.push(other_parameter);
}

// We've reached a variadic parameter in `self` which means there can
// be no more positional parameters after this in a valid AST. But, the
// current parameter in `other` is a positional-only which means there
Expand All @@ -4674,14 +4697,17 @@ impl<'db> GeneralCallableType<'db> {
let Some(other_parameter) = parameters.peek_other() else {
break;
};
if !matches!(
other_parameter.kind(),
match other_parameter.kind() {
ParameterKind::PositionalOrKeyword { .. } => {
other_keywords.push(other_parameter);
}
ParameterKind::PositionalOnly { .. }
| ParameterKind::Variadic { .. }
) {
// Any other parameter kind cannot be checked against a
// variadic parameter and is deferred to the next iteration.
break;
| ParameterKind::Variadic { .. } => {}
_ => {
// Any other parameter kind cannot be checked against a
// variadic parameter and is deferred to the next iteration.
break;
}
}
if !check_types(
other_parameter.annotated_type(),
Expand Down Expand Up @@ -4753,11 +4779,15 @@ impl<'db> GeneralCallableType<'db> {
}
}

for other_parameter in other_parameters {
for other_parameter in other_keywords.into_iter().chain(other_parameters) {
match other_parameter.kind() {
ParameterKind::KeywordOnly {
name: other_name,
default_type: other_default,
}
| ParameterKind::PositionalOrKeyword {
name: other_name,
default_type: other_default,
} => {
if let Some(self_parameter) = self_keywords.remove(other_name) {
match self_parameter.kind() {
Expand Down
Loading