mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-25 22:29:02 +00:00
[ty] Filter overloads using variadic parameters (#20547)
## Summary Closes: https://github.com/astral-sh/ty/issues/551 This PR adds support for step 4 of the overload call evaluation algorithm which states that: > If the argument list is compatible with two or more overloads, determine whether one or more of the overloads has a variadic parameter (either `*args` or `**kwargs`) that maps to a corresponding argument that supplies an indeterminate number of positional or keyword arguments. If so, eliminate overloads that do not have a variadic parameter. And, with that, the overload call evaluation algorithm has been implemented completely end to end as stated in the typing spec. ## Test Plan Expand the overload call test suite.
This commit is contained in:
parent
b0bdf0334e
commit
35ed55ec8c
4 changed files with 238 additions and 16 deletions
|
|
@ -159,6 +159,8 @@ def _(args: list[int]) -> None:
|
|||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two(*b"ab")
|
||||
takes_two(*b"abc") # error: [too-many-positional-arguments]
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
|
|
|
|||
|
|
@ -931,6 +931,134 @@ def _(t: tuple[int, str] | tuple[int, str, int]) -> None:
|
|||
f(*t) # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
## Filtering based on variaidic arguments
|
||||
|
||||
This is step 4 of the overload call evaluation algorithm which specifies that:
|
||||
|
||||
> If the argument list is compatible with two or more overloads, determine whether one or more of
|
||||
> the overloads has a variadic parameter (either `*args` or `**kwargs`) that maps to a corresponding
|
||||
> argument that supplies an indeterminate number of positional or keyword arguments. If so,
|
||||
> eliminate overloads that do not have a variadic parameter.
|
||||
|
||||
This is only performed if the previous step resulted in more than one matching overload.
|
||||
|
||||
### Simple `*args`
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(x1: int) -> tuple[int]: ...
|
||||
@overload
|
||||
def f(x1: int, x2: int) -> tuple[int, int]: ...
|
||||
@overload
|
||||
def f(*args: int) -> int: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from overloaded import f
|
||||
|
||||
def _(x1: int, x2: int, args: list[int]):
|
||||
reveal_type(f(x1)) # revealed: tuple[int]
|
||||
reveal_type(f(x1, x2)) # revealed: tuple[int, int]
|
||||
reveal_type(f(*(x1, x2))) # revealed: tuple[int, int]
|
||||
|
||||
# Step 4 should filter out all but the last overload.
|
||||
reveal_type(f(*args)) # revealed: int
|
||||
```
|
||||
|
||||
### Variable `*args`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(x1: int) -> tuple[int]: ...
|
||||
@overload
|
||||
def f(x1: int, x2: int) -> tuple[int, int]: ...
|
||||
@overload
|
||||
def f(x1: int, *args: int) -> tuple[int, ...]: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from overloaded import f
|
||||
|
||||
def _(x1: int, x2: int, args1: list[int], args2: tuple[int, *tuple[int, ...]]):
|
||||
reveal_type(f(x1, x2)) # revealed: tuple[int, int]
|
||||
reveal_type(f(*(x1, x2))) # revealed: tuple[int, int]
|
||||
|
||||
# Step 4 should filter out all but the last overload.
|
||||
reveal_type(f(x1, *args1)) # revealed: tuple[int, ...]
|
||||
reveal_type(f(*args2)) # revealed: tuple[int, ...]
|
||||
```
|
||||
|
||||
### Simple `**kwargs`
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(*, x1: int) -> int: ...
|
||||
@overload
|
||||
def f(*, x1: int, x2: int) -> tuple[int, int]: ...
|
||||
@overload
|
||||
def f(**kwargs: int) -> int: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from overloaded import f
|
||||
|
||||
def _(x1: int, x2: int, kwargs: dict[str, int]):
|
||||
reveal_type(f(x1=x1)) # revealed: int
|
||||
reveal_type(f(x1=x1, x2=x2)) # revealed: tuple[int, int]
|
||||
|
||||
# Step 4 should filter out all but the last overload.
|
||||
reveal_type(f(**{"x1": x1, "x2": x2})) # revealed: int
|
||||
reveal_type(f(**kwargs)) # revealed: int
|
||||
```
|
||||
|
||||
### `TypedDict`
|
||||
|
||||
The keys in a `TypedDict` are static so there's no variable part to it, so step 4 shouldn't filter
|
||||
out any overloads.
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import TypedDict, overload
|
||||
|
||||
@overload
|
||||
def f(*, x: int) -> int: ...
|
||||
@overload
|
||||
def f(*, x: int, y: int) -> tuple[int, int]: ...
|
||||
@overload
|
||||
def f(**kwargs: int) -> tuple[int, ...]: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
from overloaded import f
|
||||
|
||||
class Foo(TypedDict):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def _(foo: Foo, kwargs: dict[str, int]):
|
||||
reveal_type(f(**foo)) # revealed: tuple[int, int]
|
||||
reveal_type(f(**kwargs)) # revealed: tuple[int, ...]
|
||||
```
|
||||
|
||||
## Filtering based on `Any` / `Unknown`
|
||||
|
||||
This is the step 5 of the overload call evaluation algorithm which specifies that:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue