[red-knot] Support overloads for callable equivalence (#17698)

## Summary

Part of #15383, this PR adds `is_equivalent_to` support for overloaded
callables.

This is mainly done by delegating it to the subtyping check in that two
types A and B are considered equivalent if A is a subtype of B and B is
a subtype of A.

## Test Plan

Add test cases for overloaded callables in `is_equivalent_to.md`
This commit is contained in:
Dhruv Manilawala 2025-04-30 02:53:59 +05:30 committed by GitHub
parent 549ab74bd6
commit f11d9cb509
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 4 deletions

View File

@ -256,6 +256,65 @@ static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str
### Overloads ### Overloads
TODO #### One overload
`overloaded.pyi`:
```pyi
from typing import overload
class Grandparent: ...
class Parent(Grandparent): ...
class Child(Parent): ...
@overload
def overloaded(a: Child) -> None: ...
@overload
def overloaded(a: Parent) -> None: ...
@overload
def overloaded(a: Grandparent) -> None: ...
```
```py
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
from overloaded import Grandparent, Parent, Child, overloaded
def grandparent(a: Grandparent) -> None: ...
static_assert(is_equivalent_to(CallableTypeOf[grandparent], CallableTypeOf[overloaded]))
static_assert(is_equivalent_to(CallableTypeOf[overloaded], CallableTypeOf[grandparent]))
```
#### Both overloads
`overloaded.pyi`:
```pyi
from typing import overload
class Grandparent: ...
class Parent(Grandparent): ...
class Child(Parent): ...
@overload
def pg(a: Parent) -> None: ...
@overload
def pg(a: Grandparent) -> None: ...
@overload
def cpg(a: Child) -> None: ...
@overload
def cpg(a: Parent) -> None: ...
@overload
def cpg(a: Grandparent) -> None: ...
```
```py
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
from overloaded import pg, cpg
static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg]))
static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg]))
```
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent

View File

@ -6984,11 +6984,22 @@ impl<'db> CallableType<'db> {
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
match (&**self.signatures(db), &**other.signatures(db)) { match (&**self.signatures(db), &**other.signatures(db)) {
([self_signature], [other_signature]) => { ([self_signature], [other_signature]) => {
// Common case: both callable types contain a single signature, use the custom
// equivalence check instead of delegating it to the subtype check.
self_signature.is_equivalent_to(db, other_signature) self_signature.is_equivalent_to(db, other_signature)
} }
_ => { (self_signatures, other_signatures) => {
// TODO: overloads if !self_signatures
false .iter()
.chain(other_signatures.iter())
.all(|signature| signature.is_fully_static(db))
{
return false;
}
if self == other {
return true;
}
self.is_subtype_of(db, other) && other.is_subtype_of(db, self)
} }
} }
} }