[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:
Dhruv Manilawala 2025-12-15 11:04:28 +05:30 committed by GitHub
parent 04f9949711
commit ba47349c2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 0 deletions

View File

@ -670,3 +670,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str,
# error: [invalid-argument-type] # error: [invalid-argument-type]
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str] 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)
```

View File

@ -1072,6 +1072,29 @@ impl<'db> Signature<'db> {
let mut check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| { let mut check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
let type1 = type1.unwrap_or(Type::unknown()); let type1 = type1.unwrap_or(Type::unknown());
let type2 = type2.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 !result
.intersect( .intersect(
db, db,