mirror of https://github.com/astral-sh/ruff
[ty] Use `ParamSpec` without the attr for inferable check (#21934)
## Summary fixes: https://github.com/astral-sh/ty/issues/1820 ## Test Plan Add new mdtests. Ecosystem changes removes all false positives.
This commit is contained in:
parent
04f9949711
commit
ba47349c2e
|
|
@ -670,3 +670,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str,
|
|||
# error: [invalid-argument-type]
|
||||
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
|
||||
```
|
||||
|
||||
## ParamSpec attribute assignability
|
||||
|
||||
When comparing signatures with `ParamSpec` attributes (`P.args` and `P.kwargs`), two different
|
||||
inferable `ParamSpec` attributes with the same kind are assignable to each other. This enables
|
||||
method overrides where both methods have their own `ParamSpec`.
|
||||
|
||||
### Same attribute kind, both inferable
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class Parent:
|
||||
def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]:
|
||||
return callback
|
||||
|
||||
class Child1(Parent):
|
||||
# This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs
|
||||
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
|
||||
return callback
|
||||
|
||||
# Both signatures use ParamSpec, so they should be compatible
|
||||
def outer[**P](f: Callable[P, int]) -> Callable[P, int]:
|
||||
def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]:
|
||||
return g
|
||||
return inner(f)
|
||||
```
|
||||
|
||||
We can explicitly mark it as an override using the `@override` decorator.
|
||||
|
||||
```py
|
||||
from typing import override
|
||||
|
||||
class Child2(Parent):
|
||||
@override
|
||||
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
|
||||
return callback
|
||||
```
|
||||
|
||||
### One `ParamSpec` not inferable
|
||||
|
||||
Here, `P` is in a non-inferable position while `Q` is inferable. So, they are not considered
|
||||
assignable.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class Container[**P]:
|
||||
def method(self, f: Callable[P, None]) -> Callable[P, None]:
|
||||
return f
|
||||
|
||||
def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
|
||||
# error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
|
||||
return self.method(f)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1072,6 +1072,29 @@ impl<'db> Signature<'db> {
|
|||
let mut check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
|
||||
let type1 = type1.unwrap_or(Type::unknown());
|
||||
let type2 = type2.unwrap_or(Type::unknown());
|
||||
|
||||
match (type1, type2) {
|
||||
// This is a special case where the _same_ components of two different `ParamSpec`
|
||||
// type variables are assignable to each other when they're both in an inferable
|
||||
// position.
|
||||
//
|
||||
// `ParamSpec` type variables can only occur in parameter lists so this special case
|
||||
// is present here instead of in `Type::has_relation_to_impl`.
|
||||
(Type::TypeVar(typevar1), Type::TypeVar(typevar2))
|
||||
if typevar1.paramspec_attr(db).is_some()
|
||||
&& typevar1.paramspec_attr(db) == typevar2.paramspec_attr(db)
|
||||
&& typevar1
|
||||
.without_paramspec_attr(db)
|
||||
.is_inferable(db, inferable)
|
||||
&& typevar2
|
||||
.without_paramspec_attr(db)
|
||||
.is_inferable(db, inferable) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
!result
|
||||
.intersect(
|
||||
db,
|
||||
|
|
|
|||
Loading…
Reference in New Issue