diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 38e7a50426..642ee74af6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -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: diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 774f6fd0ce..d76923592f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4644,6 +4644,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 @@ -4653,6 +4657,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, .. } => { @@ -4727,7 +4739,11 @@ 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(), @@ -4735,6 +4751,13 @@ impl<'db> GeneralCallableType<'db> { 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 @@ -4749,14 +4772,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(), @@ -4828,11 +4854,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() {