From f11d9cb509fa823b9c52ff799db7077199472677 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 02:53:59 +0530 Subject: [PATCH] [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` --- .../type_properties/is_equivalent_to.md | 61 ++++++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 17 +++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 3a234bb8a6..69efe5958a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -256,6 +256,65 @@ static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str ### 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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index dc7a789939..1047e929f0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6984,11 +6984,22 @@ impl<'db> CallableType<'db> { fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { match (&**self.signatures(db), &**other.signatures(db)) { ([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) } - _ => { - // TODO: overloads - false + (self_signatures, other_signatures) => { + if !self_signatures + .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) } } }