[red-knot] Synthesize a `__call__` attribute for Callable types (#17809)

## Summary

Currently this assertion fails on `main`, because we do not synthesize a
`__call__` attribute for Callable types:

```py
from typing import Protocol, Callable
from knot_extensions import static_assert, is_assignable_to

class Foo(Protocol):
    def __call__(self, x: int, /) -> str: ...

static_assert(is_assignable_to(Callable[[int], str], Foo))
```

This PR fixes that.

See previous discussion about this in
https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 and
https://github.com/astral-sh/ruff/pull/17682#issuecomment-2839527750

## Test Plan

Existing mdtests updated; a couple of new ones added.
This commit is contained in:
Alex Waygood 2025-05-03 16:43:18 +01:00 committed by GitHub
parent c4a08782cc
commit 52b0470870
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 24 additions and 19 deletions

View File

@ -300,12 +300,7 @@ from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: def __init__(self) -> None
reveal_type(c.__class__) # revealed: type
# TODO: The member lookup for `Callable` uses `object` which does not have a `__call__`
# attribute. We could special case `__call__` in this context. Refer to
# https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 for more details.
# error: [unresolved-attribute] "Type `(int, /) -> int` has no attribute `__call__`"
reveal_type(c.__call__) # revealed: Unknown
reveal_type(c.__call__) # revealed: (int, /) -> int
```
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form

View File

@ -1476,26 +1476,32 @@ signature implied by the `Callable` type is assignable to the signature of the `
specified by the protocol:
```py
from knot_extensions import TypeOf
class Foo(Protocol):
def __call__(self, x: int, /) -> str: ...
# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method,
# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass.
static_assert(is_subtype_of(Callable[[int], str], Foo)) # error: [static-assert-error]
static_assert(is_assignable_to(Callable[[int], str], Foo)) # error: [static-assert-error]
static_assert(is_subtype_of(Callable[[int], str], Foo))
static_assert(is_assignable_to(Callable[[int], str], Foo))
static_assert(not is_subtype_of(Callable[[str], str], Foo))
static_assert(not is_assignable_to(Callable[[str], str], Foo))
static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo))
static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo))
# TODO: these should pass
static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error]
static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error]
static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error]
static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error]
def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]):
# TODO: this fails because we don't yet understand that all `Callable` types have a `__call__` method,
# and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass.
obj2 = obj # error: [invalid-assignment]
obj2 = obj
# This diagnostic is correct, however.
obj2 = obj3 # error: [invalid-assignment]
# TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable
# to the declared type of `obj2`
obj2 = obj3
def satisfies_foo(x: int) -> str:
return "foo"
static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo))
static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo))
```
## Protocols are never singleton types, and are never single-valued types

View File

@ -2953,6 +2953,10 @@ impl<'db> Type<'db> {
Type::DataclassDecorator(_) => KnownClass::FunctionType
.to_instance(db)
.member_lookup_with_policy(db, name, policy),
Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => {
Symbol::bound(self).into()
}
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object
.to_instance(db)
.member_lookup_with_policy(db, name, policy),