mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:56 +00:00
[red-knot] Check subtype relation between callable types (#16804)
## Summary Part of #15382 This PR adds support for checking the subtype relationship between the two callable types. The main source of reference used for implementation is https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables. The implementation is split into two phases: 1. Check all the positional parameters which includes positional-only, standard (positional or keyword) and variadic kind 2. Collect all the keywords in a `HashMap` to do the keyword parameters check via name lookup For (1), there's a helper struct which is similar to `.zip_longest` (from `itertools`) except that it allows control over one of the iterator as that's required when processing a variadic parameter. This is required because positional parameters needs to be checked as per their position between the two callable types. The struct also keeps track of the current iteration element because when the loop is exited (to move on to the phase 2) the current iteration element would be carried over to the phase 2 check. This struct is internal to the `is_subtype_of` method as I don't think it makes sense to expose it outside. It also allows me to use "self" and "other" suffixed field names as that's only relevant in that context. ## Test Plan Add extensive tests in markdown. Converted all of the code snippets from https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables to use `knot_extensions.is_subtype_of` and verified the result.
This commit is contained in:
parent
193c38199e
commit
04a8756379
2 changed files with 856 additions and 2 deletions
|
@ -478,5 +478,515 @@ static_assert(not is_subtype_of(Intersection[Unknown, int], int))
|
|||
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown]))
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
The general principle is that a callable type is a subtype of another if it's more flexible in what
|
||||
it accepts and more specific in what it returns.
|
||||
|
||||
References:
|
||||
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#assignability-rules-for-callables>
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#assignment>
|
||||
|
||||
### Return type
|
||||
|
||||
Return types are covariant.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(Callable[[], int], Callable[[], float]))
|
||||
static_assert(not is_subtype_of(Callable[[], float], Callable[[], int]))
|
||||
```
|
||||
|
||||
### Parameter types
|
||||
|
||||
Parameter types are contravariant.
|
||||
|
||||
#### Positional-only
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def float_param(a: float, /) -> None: ...
|
||||
def int_param(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
```
|
||||
|
||||
Parameter name is not required to be the same for positional-only parameters at the same position:
|
||||
|
||||
```py
|
||||
def int_param_different_name(b: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_param_different_name]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param_different_name], CallableTypeFromFunction[int_param]))
|
||||
```
|
||||
|
||||
Multiple positional-only parameters are checked in order:
|
||||
|
||||
```py
|
||||
def multi_param1(a: float, b: int, c: str, /) -> None: ...
|
||||
def multi_param2(b: int, c: bool, a: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
```
|
||||
|
||||
#### Positional-only with default value
|
||||
|
||||
If the parameter has a default value, it's treated as optional. This means that the parameter at the
|
||||
corresponding position in the supertype does not need to have a default value.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def float_with_default(a: float = 1, /) -> None: ...
|
||||
def int_with_default(a: int = 1, /) -> None: ...
|
||||
def int_without_default(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
As the parameter itself is optional, it can be omitted in the supertype:
|
||||
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
The subtype can include any number of positional-only parameters as long as they have the default
|
||||
value:
|
||||
|
||||
```py
|
||||
def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param]))
|
||||
```
|
||||
|
||||
#### Positional-only with other kinds
|
||||
|
||||
If a parameter is declared as positional-only, then the corresponding parameter in the supertype
|
||||
cannot be any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def positional_only(a: int, /) -> None: ...
|
||||
def standard(a: int) -> None: ...
|
||||
def keyword_only(*, a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Standard
|
||||
|
||||
A standard parameter is either a positional or a keyword parameter.
|
||||
|
||||
Unlike positional-only parameters, standard parameters should have the same name in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def int_param_a(a: int) -> None: ...
|
||||
def int_param_b(b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_a], CallableTypeFromFunction[int_param_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_b], CallableTypeFromFunction[int_param_a]))
|
||||
```
|
||||
|
||||
Apart from the name, it behaves the same as positional-only parameters.
|
||||
|
||||
```py
|
||||
def float_param(a: float) -> None: ...
|
||||
def int_param(a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
```
|
||||
|
||||
With the same rules for default values as well.
|
||||
|
||||
```py
|
||||
def float_with_default(a: float = 1) -> None: ...
|
||||
def int_with_default(a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Multiple standard parameters are checked in order along with their names:
|
||||
|
||||
```py
|
||||
def multi_param1(a: float, b: int, c: str) -> None: ...
|
||||
def multi_param2(a: int, b: bool, c: str) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
```
|
||||
|
||||
The subtype can include as many standard parameters as long as they have the default value:
|
||||
|
||||
```py
|
||||
def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param_default]))
|
||||
```
|
||||
|
||||
#### Standard with keyword-only
|
||||
|
||||
A keyword-only parameter in the supertype can be substituted with the corresponding standard
|
||||
parameter in the subtype with the same name. This is because a standard parameter is more flexible
|
||||
than a keyword-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
# The name of the parameters are different
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
|
||||
# Here, the name of the parameters are the same
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[keyword_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def keyword_with_default(*, a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keyword_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the keyword-only parameters does not matter:
|
||||
|
||||
```py
|
||||
def multi_standard(a: float, b: int, c: str) -> None: ...
|
||||
def multi_keyword(*, b: bool, c: str, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_keyword]))
|
||||
```
|
||||
|
||||
#### Standard with positional-only
|
||||
|
||||
A positional-only parameter in the supertype can be substituted with the corresponding standard
|
||||
parameter in the subtype at the same position. This is because a standard parameter is more flexible
|
||||
than a positional-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def positional_b(b: int, /) -> None: ...
|
||||
|
||||
# The names are not important in this context
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[positional_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def positional_int(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[positional_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def positional_with_default(a: int = 1, /) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[positional_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the positional-only parameters matter:
|
||||
|
||||
```py
|
||||
def multi_standard(a: float, b: int, c: str) -> None: ...
|
||||
def multi_positional1(b: int, c: bool, a: str, /) -> None: ...
|
||||
|
||||
# Here, the type of the parameter `a` makes the subtype relation invalid
|
||||
def multi_positional2(b: int, a: float, c: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional2]))
|
||||
```
|
||||
|
||||
#### Standard with variadic
|
||||
|
||||
A variadic or keyword-variadic parameter in the supertype cannot be substituted with a standard
|
||||
parameter in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard(a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic
|
||||
|
||||
The name of the variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic_float(*args2: float) -> None: ...
|
||||
def variadic_int(*args1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_float], CallableTypeFromFunction[variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[variadic_float]))
|
||||
```
|
||||
|
||||
The variadic parameter does not need to be present in the supertype:
|
||||
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[variadic_int]))
|
||||
```
|
||||
|
||||
#### Variadic with positional-only
|
||||
|
||||
If the subtype has a variadic parameter then any unmatched positional-only parameter from the
|
||||
supertype should be checked against the variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(a: int, /, *args: float) -> None: ...
|
||||
|
||||
# Here, the parameter `b` and `c` are unmatched
|
||||
def positional_only(a: int, b: float, c: int, /) -> None: ...
|
||||
|
||||
# Here, the parameter `b` is unmatched and there's also a variadic parameter
|
||||
def positional_variadic(a: int, b: float, /, *args: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic with other kinds
|
||||
|
||||
Variadic parameter in a subtype can only be used to match against an unmatched positional-only
|
||||
parameters from the supertype, not any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(*args: int) -> None: ...
|
||||
|
||||
# Both positional-only parameters are unmatched so uses the variadic parameter but the other
|
||||
# parameter `c` remains and cannot be matched.
|
||||
def standard(a: int, b: float, /, c: int) -> None: ...
|
||||
|
||||
# Similarly, for other kinds
|
||||
def keyword_only(a: int, /, *, b: int) -> None: ...
|
||||
def keyword_variadic(a: int, /, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Keyword-only
|
||||
|
||||
For keyword-only parameters, the name should be the same:
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
def keyword_float(*, a: float) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_float], CallableTypeFromFunction[keyword_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_float]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_b]))
|
||||
```
|
||||
|
||||
But, the order of the keyword-only parameters is not required to be the same:
|
||||
|
||||
```py
|
||||
def keyword_ab(*, a: float, b: float) -> None: ...
|
||||
def keyword_ba(*, b: int, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_ab], CallableTypeFromFunction[keyword_ba]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_ba], CallableTypeFromFunction[keyword_ab]))
|
||||
```
|
||||
|
||||
#### Keyword-only with default
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def float_with_default(*, a: float = 1) -> None: ...
|
||||
def int_with_default(*, a: int = 1) -> None: ...
|
||||
def int_keyword(*, a: int) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Keyword-only parameters with default values can be mixed with the ones without default values in any
|
||||
order:
|
||||
|
||||
```py
|
||||
# A keyword-only parameter with a default value follows the one without a default value (it's valid)
|
||||
def mixed(*, b: int = 1, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
#### Keyword-only with standard
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keywords1(*, a: int, b: int) -> None: ...
|
||||
def standard(b: float, a: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords1], CallableTypeFromFunction[standard]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
The subtype can include additional standard parameters as long as it has the default value:
|
||||
|
||||
```py
|
||||
def standard_with_default(b: float, a: float, c: float = 1) -> None: ...
|
||||
def standard_without_default(b: float, a: float, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_without_default], CallableTypeFromFunction[keywords1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
Here, we mix keyword-only parameters with standard parameters:
|
||||
|
||||
```py
|
||||
def keywords2(*, a: int, c: int, b: int) -> None: ...
|
||||
def mixed(b: float, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords2], CallableTypeFromFunction[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, we shouldn't consider any unmatched positional-only parameters:
|
||||
|
||||
```py
|
||||
def mixed_positional(b: float, /, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, an unmatched variadic parameter is still valid:
|
||||
|
||||
```py
|
||||
def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed_variadic], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic
|
||||
|
||||
The name of the keyword-variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs_float(**kwargs2: float) -> None: ...
|
||||
def kwargs_int(**kwargs1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_float], CallableTypeFromFunction[kwargs_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[kwargs_float]))
|
||||
```
|
||||
|
||||
A variadic parameter can be omitted in the subtype:
|
||||
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[kwargs_int]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic with keyword-only
|
||||
|
||||
If the subtype has a keyword-variadic parameter then any unmatched keyword-only parameter from the
|
||||
supertype should be checked against the keyword-variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs(**kwargs: float) -> None: ...
|
||||
def keyword_only(*, a: int, b: float, c: bool) -> None: ...
|
||||
def keyword_variadic(*, a: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
This is valid only for keyword-only parameters, not any other parameter kind:
|
||||
|
||||
```py
|
||||
def mixed1(a: int, *, b: int) -> None: ...
|
||||
|
||||
# Same as above but with the default value
|
||||
def mixed2(a: int = 1, *, b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed2]))
|
||||
```
|
||||
|
||||
#### Empty
|
||||
|
||||
When the supertype has an empty list of parameters, then the subtype can have any kind of parameters
|
||||
as long as they contain the default values for non-variadic parameters.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def empty() -> None: ...
|
||||
def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
[special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use std::hash::Hash;
|
||||
use std::collections::HashMap;
|
||||
use std::slice::Iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use call::{CallDunderError, CallError, CallErrorKind};
|
||||
use context::InferContext;
|
||||
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
|
||||
use itertools::EitherOrBoth;
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
@ -654,8 +656,14 @@ impl<'db> Type<'db> {
|
|||
.is_subtype_of(db, target)
|
||||
}
|
||||
|
||||
(
|
||||
Type::Callable(CallableType::General(self_callable)),
|
||||
Type::Callable(CallableType::General(other_callable)),
|
||||
) => self_callable.is_subtype_of(db, other_callable),
|
||||
|
||||
(Type::Callable(CallableType::General(_)), _) => {
|
||||
// TODO: Implement subtyping for general callable types
|
||||
// TODO: Implement subtyping between general callable types and other types like
|
||||
// function literals, bound methods, class literals, `type[]`, etc.)
|
||||
false
|
||||
}
|
||||
|
||||
|
@ -4738,6 +4746,342 @@ impl<'db> GeneralCallableType<'db> {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
/// A helper struct to zip two slices of parameters together that provides control over the
|
||||
/// two iterators individually. It also keeps track of the current parameter in each
|
||||
/// iterator.
|
||||
struct ParametersZip<'a, 'db> {
|
||||
current_self: Option<&'a Parameter<'db>>,
|
||||
current_other: Option<&'a Parameter<'db>>,
|
||||
iter_self: Iter<'a, Parameter<'db>>,
|
||||
iter_other: Iter<'a, Parameter<'db>>,
|
||||
}
|
||||
|
||||
impl<'a, 'db> ParametersZip<'a, 'db> {
|
||||
/// Move to the next parameter in both the `self` and `other` parameter iterators,
|
||||
/// [`None`] if both iterators are exhausted.
|
||||
fn next(&mut self) -> Option<EitherOrBoth<&'a Parameter<'db>, &'a Parameter<'db>>> {
|
||||
match (self.next_self(), self.next_other()) {
|
||||
(Some(self_param), Some(other_param)) => {
|
||||
Some(EitherOrBoth::Both(self_param, other_param))
|
||||
}
|
||||
(Some(self_param), None) => Some(EitherOrBoth::Left(self_param)),
|
||||
(None, Some(other_param)) => Some(EitherOrBoth::Right(other_param)),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Move to the next parameter in the `self` parameter iterator, [`None`] if the
|
||||
/// iterator is exhausted.
|
||||
fn next_self(&mut self) -> Option<&'a Parameter<'db>> {
|
||||
self.current_self = self.iter_self.next();
|
||||
self.current_self
|
||||
}
|
||||
|
||||
/// Move to the next parameter in the `other` parameter iterator, [`None`] if the
|
||||
/// iterator is exhausted.
|
||||
fn next_other(&mut self) -> Option<&'a Parameter<'db>> {
|
||||
self.current_other = self.iter_other.next();
|
||||
self.current_other
|
||||
}
|
||||
|
||||
/// Peek at the next parameter in the `other` parameter iterator without consuming it.
|
||||
fn peek_other(&mut self) -> Option<&'a Parameter<'db>> {
|
||||
self.iter_other.clone().next()
|
||||
}
|
||||
|
||||
/// Consumes the `ParametersZip` and returns a two-element tuple containing the
|
||||
/// remaining parameters in the `self` and `other` iterators respectively.
|
||||
///
|
||||
/// The returned iterators starts with the current parameter, if any, followed by the
|
||||
/// remaining parameters in the respective iterators.
|
||||
fn into_remaining(
|
||||
self,
|
||||
) -> (
|
||||
impl Iterator<Item = &'a Parameter<'db>>,
|
||||
impl Iterator<Item = &'a Parameter<'db>>,
|
||||
) {
|
||||
(
|
||||
self.current_self.into_iter().chain(self.iter_self),
|
||||
self.current_other.into_iter().chain(self.iter_other),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let self_signature = self.signature(db);
|
||||
let other_signature = other.signature(db);
|
||||
|
||||
// Check if `type1` is a subtype of `type2`. This is mainly to avoid `unwrap` calls
|
||||
// scattered throughout the function.
|
||||
let is_subtype = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
|
||||
// SAFETY: Subtype relation is only checked for fully static types.
|
||||
type1.unwrap().is_subtype_of(db, type2.unwrap())
|
||||
};
|
||||
|
||||
// Return types are covariant.
|
||||
if !is_subtype(self_signature.return_ty, other_signature.return_ty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut parameters = ParametersZip {
|
||||
current_self: None,
|
||||
current_other: None,
|
||||
iter_self: self_signature.parameters().iter(),
|
||||
iter_other: other_signature.parameters().iter(),
|
||||
};
|
||||
|
||||
loop {
|
||||
let Some(next_parameter) = parameters.next() else {
|
||||
// All parameters have been checked or both the parameter lists were empty. In
|
||||
// either case, `self` is a subtype of `other`.
|
||||
return true;
|
||||
};
|
||||
|
||||
match next_parameter {
|
||||
EitherOrBoth::Left(self_parameter) => match self_parameter.kind() {
|
||||
ParameterKind::PositionalOnly { default_ty, .. }
|
||||
| ParameterKind::PositionalOrKeyword { default_ty, .. }
|
||||
| ParameterKind::KeywordOnly { default_ty, .. } => {
|
||||
// For `self <: other` to be valid, if there are no more parameters in
|
||||
// `other`, then the non-variadic parameters in `self` must have a default
|
||||
// value.
|
||||
if default_ty.is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => {
|
||||
// Variadic parameters don't have any restrictions in this context, so
|
||||
// we'll just continue to the next parameter set.
|
||||
}
|
||||
},
|
||||
|
||||
EitherOrBoth::Right(_) => {
|
||||
// If there are more parameters in `other` than in `self`, then `self` is not a
|
||||
// subtype of `other`.
|
||||
return false;
|
||||
}
|
||||
|
||||
EitherOrBoth::Both(self_parameter, other_parameter) => {
|
||||
match (self_parameter.kind(), other_parameter.kind()) {
|
||||
(
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: self_default,
|
||||
..
|
||||
}
|
||||
| ParameterKind::PositionalOrKeyword {
|
||||
default_ty: self_default,
|
||||
..
|
||||
},
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: other_default,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
if self_default.is_none() && other_default.is_some() {
|
||||
return false;
|
||||
}
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: self_name,
|
||||
default_ty: self_default,
|
||||
},
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: other_name,
|
||||
default_ty: other_default,
|
||||
},
|
||||
) => {
|
||||
if self_name != other_name {
|
||||
return false;
|
||||
}
|
||||
// The following checks are the same as positional-only parameters.
|
||||
if self_default.is_none() && other_default.is_some() {
|
||||
return false;
|
||||
}
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(ParameterKind::Variadic { .. }, ParameterKind::PositionalOnly { .. }) => {
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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
|
||||
// can be more positional parameters after this which could be either
|
||||
// more positional-only parameters, standard parameters or a variadic
|
||||
// parameter.
|
||||
//
|
||||
// So, any remaining positional parameters in `other` would need to be
|
||||
// checked against the variadic parameter in `self`. This loop does
|
||||
// that by only moving the `other` iterator forward.
|
||||
loop {
|
||||
let Some(other_parameter) = parameters.peek_other() else {
|
||||
break;
|
||||
};
|
||||
if !matches!(
|
||||
other_parameter.kind(),
|
||||
ParameterKind::PositionalOnly { .. }
|
||||
| ParameterKind::Variadic { .. }
|
||||
) {
|
||||
// Any other parameter kind cannot be checked against a
|
||||
// variadic parameter and is deferred to the next iteration.
|
||||
break;
|
||||
}
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
parameters.next_other();
|
||||
}
|
||||
}
|
||||
|
||||
(ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
_,
|
||||
ParameterKind::KeywordOnly { .. }
|
||||
| ParameterKind::KeywordVariadic { .. },
|
||||
) => {
|
||||
// Keyword parameters are not considered in this loop as the order of
|
||||
// parameters is not important for them and so they are checked by
|
||||
// doing name-based lookups.
|
||||
break;
|
||||
}
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the remaining parameters in `other` are keyword-only or keyword variadic.
|
||||
// But, `self` could contain any unmatched positional parameters.
|
||||
let (self_parameters, other_parameters) = parameters.into_remaining();
|
||||
|
||||
// Collect all the keyword-only parameters and the unmatched standard parameters.
|
||||
let mut self_keywords = HashMap::new();
|
||||
|
||||
// Type of the variadic keyword parameter in `self`.
|
||||
//
|
||||
// This is a nested option where the outer option represents the presence of a keyword
|
||||
// variadic parameter in `self` and the inner option represents the annotated type of the
|
||||
// keyword variadic parameter.
|
||||
let mut self_keyword_variadic: Option<Option<Type<'db>>> = None;
|
||||
|
||||
for self_parameter in self_parameters {
|
||||
match self_parameter.kind() {
|
||||
ParameterKind::KeywordOnly { name, .. }
|
||||
| ParameterKind::PositionalOrKeyword { name, .. } => {
|
||||
self_keywords.insert(name.clone(), self_parameter);
|
||||
}
|
||||
ParameterKind::KeywordVariadic { .. } => {
|
||||
self_keyword_variadic = Some(self_parameter.annotated_type());
|
||||
}
|
||||
ParameterKind::PositionalOnly { .. } => {
|
||||
// These are the unmatched positional-only parameters in `self` from the
|
||||
// previous loop. They cannot be matched against any parameter in `other` which
|
||||
// only contains keyword-only and keyword-variadic parameters so the subtype
|
||||
// relation is invalid.
|
||||
return false;
|
||||
}
|
||||
ParameterKind::Variadic { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
for other_parameter in other_parameters {
|
||||
match other_parameter.kind() {
|
||||
ParameterKind::KeywordOnly {
|
||||
name: other_name,
|
||||
default_ty: other_default,
|
||||
} => {
|
||||
if let Some(self_parameter) = self_keywords.remove(other_name) {
|
||||
match self_parameter.kind() {
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: self_default,
|
||||
..
|
||||
}
|
||||
| ParameterKind::KeywordOnly {
|
||||
default_ty: self_default,
|
||||
..
|
||||
} => {
|
||||
if self_default.is_none() && other_default.is_some() {
|
||||
return false;
|
||||
}
|
||||
if !is_subtype(
|
||||
other_parameter.annotated_type(),
|
||||
self_parameter.annotated_type(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(
|
||||
"`self_keywords` should only contain keyword-only or standard parameters"
|
||||
),
|
||||
}
|
||||
} else if let Some(self_keyword_variadic_type) = self_keyword_variadic {
|
||||
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ParameterKind::KeywordVariadic { .. } => {
|
||||
let Some(self_keyword_variadic_type) = self_keyword_variadic else {
|
||||
// For a `self <: other` relationship, if `other` has a keyword variadic
|
||||
// parameter, `self` must also have a keyword variadic parameter.
|
||||
return false;
|
||||
};
|
||||
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// This can only occur in case of a syntax error.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are still unmatched keyword parameters from `self`, then they should be
|
||||
// optional otherwise the subtype relation is invalid.
|
||||
for (_, self_parameter) in self_keywords {
|
||||
if self_parameter.default_type().is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that represents callable objects.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue