From 40600821eaa06db61db9e34a9af9afa3457a43f1 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 3 Oct 2025 10:15:34 -0400 Subject: [PATCH] don't consider self --- .../mdtest/type_properties/is_subtype_of.md | 125 +++++++++++++++++- crates/ty_python_semantic/src/types.rs | 37 +++--- 2 files changed, 140 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 4320b56e72..fa527968a7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1963,6 +1963,10 @@ static_assert(not is_subtype_of(type[A], Callable[[int], A])) ### Bound methods +The bound `self` or `cls` parameter of a bound method has been curried away, and is no longer +available as a parameter when calling it. It therefore is not considered when checking subtyping +involving the bound method. + ```py from typing import Callable from ty_extensions import TypeOf, static_assert, is_subtype_of @@ -1981,12 +1985,131 @@ static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int])) -static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int])) static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int])) static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int])) ``` +Return types are in covariant position: + +```py +static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], object])) +static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], object])) +static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], object])) + +static_assert(not is_subtype_of(TypeOf[a.f], Callable[[int], bool])) +static_assert(not is_subtype_of(TypeOf[a.g], Callable[[int], bool])) +static_assert(not is_subtype_of(TypeOf[A.g], Callable[[int], bool])) +``` + +Parameter types are in contravariant position: + +```py +static_assert(not is_subtype_of(TypeOf[a.f], Callable[[object], int])) +static_assert(not is_subtype_of(TypeOf[a.g], Callable[[object], int])) +static_assert(not is_subtype_of(TypeOf[A.g], Callable[[object], int])) + +static_assert(is_subtype_of(TypeOf[a.f], Callable[[bool], int])) +static_assert(is_subtype_of(TypeOf[a.g], Callable[[bool], int])) +static_assert(is_subtype_of(TypeOf[A.g], Callable[[bool], int])) +``` + +We should get the same results when comparing with a bound method of a different class. + +```py +class B: + def returns_int(self, a: int) -> int: + return 0 + + def returns_object(self, a: int) -> object: + return a + + def returns_bool(self, a: int) -> bool: + return True + + def takes_object(self, a: object) -> int: + return 0 + + def takes_bool(self, a: bool) -> int: + return 0 + + @classmethod + def class_returns_int(cls, a: int) -> int: + return 0 + + @classmethod + def class_returns_object(cls, a: int) -> object: + return a + + @classmethod + def class_returns_bool(cls, a: int) -> bool: + return True + + @classmethod + def class_takes_object(cls, a: object) -> int: + return 0 + + @classmethod + def class_takes_bool(cls, a: bool) -> int: + return 0 + +b = B() + +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.returns_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.returns_int])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[b.returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[b.takes_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.takes_bool])) + +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.class_returns_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[b.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[b.class_takes_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[b.class_takes_bool])) + +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[B.class_returns_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[B.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[B.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.f], TypeOf[B.class_takes_object])) +static_assert(is_subtype_of(TypeOf[a.f], TypeOf[B.class_takes_bool])) + +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.returns_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.returns_int])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[b.returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[b.takes_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.takes_bool])) + +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.class_returns_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[b.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[b.class_takes_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[b.class_takes_bool])) + +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[B.class_returns_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[B.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[B.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[a.g], TypeOf[B.class_takes_object])) +static_assert(is_subtype_of(TypeOf[a.g], TypeOf[B.class_takes_bool])) + +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.returns_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.returns_int])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[b.returns_bool])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[b.takes_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.takes_bool])) + +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.class_returns_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[b.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[b.class_takes_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[b.class_takes_bool])) + +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[B.class_returns_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[B.class_returns_int])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[B.class_returns_bool])) +static_assert(not is_subtype_of(TypeOf[A.g], TypeOf[B.class_takes_object])) +static_assert(is_subtype_of(TypeOf[A.g], TypeOf[B.class_takes_bool])) +``` + ### Overloads #### Subtype overloaded diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 916f9471a0..b6eb0a02ae 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9165,20 +9165,16 @@ impl<'db> BoundMethodType<'db> { relation: TypeRelation, visitor: &HasRelationToVisitor<'db>, ) -> ConstraintSet<'db> { - // A bound method is a typically a subtype of itself. However, we must explicitly verify - // the subtyping of the underlying function signatures (since they might be specialized - // differently), and of the bound self parameter (taking care that parameters, including a - // bound self parameter, are contravariant.) - self.function(db) - .has_relation_to_impl(db, other.function(db), relation, visitor) - .and(db, || { - other.self_instance(db).has_relation_to_impl( - db, - self.self_instance(db), - relation, - visitor, - ) - }) + // Note that we do not consider `self_instance`, and we use `into_callable_type` to compare + // the signatures of the bound methods _after_ the `self` parameter has been bound. The + // bound `self` is no longer available as a parameter to callers, and should therefore not + // influence the result of the check. + self.into_callable_type(db).has_relation_to_impl( + db, + other.into_callable_type(db), + relation, + visitor, + ) } fn is_equivalent_to_impl( @@ -9187,13 +9183,12 @@ impl<'db> BoundMethodType<'db> { other: Self, visitor: &IsEquivalentVisitor<'db>, ) -> ConstraintSet<'db> { - self.function(db) - .is_equivalent_to_impl(db, other.function(db), visitor) - .and(db, || { - other - .self_instance(db) - .is_equivalent_to_impl(db, self.self_instance(db), visitor) - }) + // Note that we do not consider `self_instance`, and we use `into_callable_type` to compare + // the signatures of the bound methods _after_ the `self` parameter has been bound. The + // bound `self` is no longer available as a parameter to callers, and should therefore not + // influence the result of the check. + self.into_callable_type(db) + .is_equivalent_to_impl(db, other.into_callable_type(db), visitor) } }