[red-knot] Assignability of class instances to Callable (#17590)

## Summary

Model assignability of class instances with a `__call__` method to
`Callable` types. This should solve some false positives related to
`functools.partial` (yes, 1098 fewer diagnostics!).

Reference:
https://github.com/astral-sh/ruff/issues/17343#issuecomment-2824618483

## Test Plan

New Markdown tests.
This commit is contained in:
David Peter 2025-04-23 20:34:13 +02:00 committed by GitHub
parent e170fe493d
commit 61e73481fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 0 deletions

View file

@ -560,4 +560,30 @@ c: Callable[..., int] = overloaded
c: Callable[[int], str] = overloaded
```
### Classes with `__call__`
```py
from typing import Callable, Any
from knot_extensions import static_assert, is_assignable_to
class TakesAny:
def __call__(self, a: Any) -> str:
return ""
class ReturnsAny:
def __call__(self, a: str) -> Any: ...
static_assert(is_assignable_to(TakesAny, Callable[[int], str]))
static_assert(not is_assignable_to(TakesAny, Callable[[int], int]))
static_assert(is_assignable_to(ReturnsAny, Callable[[str], int]))
static_assert(not is_assignable_to(ReturnsAny, Callable[[int], int]))
from functools import partial
def f(x: int, y: str) -> None: ...
c1: Callable[[int], None] = partial(f, y="a")
```
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

View file

@ -1460,6 +1460,16 @@ impl<'db> Type<'db> {
self_callable.is_assignable_to(db, target_callable)
}
(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_assignable_to(db, target),
_ => false,
}
}
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)