From eb3e1763096bd85b37f1609b6baf1e10694143cf Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 2 Apr 2025 00:40:44 +0100 Subject: [PATCH] [red-knot] Add callable subtyping for callable instances and bound methods (#17105) ## Summary Trying to improve #17005 Partially fixes #16953 ## Test Plan Update is_subtype_of.md --------- Co-authored-by: Carl Meyer --- .../mdtest/type_properties/is_subtype_of.md | 49 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 23 +++++++++ .../src/types/signatures.rs | 7 +++ 3 files changed, 79 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 642ee74af6..307c7ec527 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1099,5 +1099,54 @@ static_assert(is_subtype_of(TypeOf[C.foo], object)) static_assert(not is_subtype_of(object, TypeOf[C.foo])) ``` +### Classes with `__call__` + +```py +from typing import Callable +from knot_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to + +class A: + def __call__(self, a: int) -> int: + return a + +a = A() + +static_assert(is_subtype_of(A, Callable[[int], int])) +static_assert(not is_subtype_of(A, Callable[[], int])) +static_assert(not is_subtype_of(Callable[[int], int], A)) + +def f(fn: Callable[[int], int]) -> None: ... + +f(a) +``` + +### Bound methods + +```py +from typing import Callable +from knot_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def f(self, a: int) -> int: + return a + + @classmethod + def g(cls, a: int) -> int: + return a + +a = A() + +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])) + +# TODO: This assertion should be true +# error: [static-assert-error] "Static assertion error: argument evaluates to `False`" +static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int])) +``` + [special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex [typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 7cd2a9ebc7..1a26ff4834 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -727,6 +727,10 @@ impl<'db> Type<'db> { .is_subtype_of(db, target) } + (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method + .into_callable_type(db) + .is_subtype_of(db, target), + // A `FunctionLiteral` type is a single-valued type like the other literals handled above, // so it also, for now, just delegates to its instance fallback. (Type::FunctionLiteral(_), _) => KnownClass::FunctionType @@ -833,6 +837,16 @@ impl<'db> Type<'db> { self_instance.is_subtype_of(db, target_instance) } + (Type::Instance(_), Type::Callable(_)) => { + let call_symbol = self.member(db, "__call__").symbol; + match call_symbol { + Symbol::Type(Type::BoundMethod(call_function), _) => call_function + .into_callable_type(db) + .is_subtype_of(db, target), + _ => false, + } + } + // Other than the special cases enumerated above, // `Instance` types are never subtypes of any other variants (Type::Instance(_), _) => false, @@ -4414,6 +4428,15 @@ pub struct BoundMethodType<'db> { self_instance: Type<'db>, } +impl<'db> BoundMethodType<'db> { + pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { + Type::Callable(CallableType::new( + db, + self.function(db).signature(db).bind_self(), + )) + } +} + /// This type represents the set of all callable objects with a certain signature. /// It can be written in type expressions using `typing.Callable`. /// `lambda` expressions are inferred directly as `CallableType`s; all function-literal types diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 0606372bec..039f6f4d8d 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -265,6 +265,13 @@ impl<'db> Signature<'db> { pub(crate) fn parameters(&self) -> &Parameters<'db> { &self.parameters } + + pub(crate) fn bind_self(&self) -> Self { + Self { + parameters: Parameters::new(self.parameters().iter().skip(1).cloned()), + return_ty: self.return_ty, + } + } } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]