[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 <carl@astral.sh>
This commit is contained in:
Matthew Mckee 2025-04-02 00:40:44 +01:00 committed by GitHub
parent d38f6fcc55
commit eb3e176309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 79 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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)]