From 61e73481feca429f51a3c02e08666202dce381fb Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 20:34:13 +0200 Subject: [PATCH] [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. --- .../type_properties/is_assignable_to.md | 26 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 10 +++++++ 2 files changed, 36 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 9c33827733..ce7198ba69 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 398cd95d1e..ebb44ae9cc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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)