Skip to content

Commit 52b0470

Browse files
authored
[red-knot] Synthesize a __call__ attribute for Callable types (astral-sh#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 astral-sh#16493 (comment) and astral-sh#17682 (comment) ## Test Plan Existing mdtests updated; a couple of new ones added.
1 parent c4a0878 commit 52b0470

File tree

3 files changed

+24
-19
lines changed

3 files changed

+24
-19
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,7 @@ from typing import Callable
300300
def _(c: Callable[[int], int]):
301301
reveal_type(c.__init__) # revealed: def __init__(self) -> None
302302
reveal_type(c.__class__) # revealed: type
303-
304-
# TODO: The member lookup for `Callable` uses `object` which does not have a `__call__`
305-
# attribute. We could special case `__call__` in this context. Refer to
306-
# https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 for more details.
307-
# error: [unresolved-attribute] "Type `(int, /) -> int` has no attribute `__call__`"
308-
reveal_type(c.__call__) # revealed: Unknown
303+
reveal_type(c.__call__) # revealed: (int, /) -> int
309304
```
310305

311306
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form

crates/red_knot_python_semantic/resources/mdtest/protocols.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,26 +1476,32 @@ signature implied by the `Callable` type is assignable to the signature of the `
14761476
specified by the protocol:
14771477

14781478
```py
1479+
from knot_extensions import TypeOf
1480+
14791481
class Foo(Protocol):
14801482
def __call__(self, x: int, /) -> str: ...
14811483

1482-
# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method,
1483-
# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass.
1484-
static_assert(is_subtype_of(Callable[[int], str], Foo)) # error: [static-assert-error]
1485-
static_assert(is_assignable_to(Callable[[int], str], Foo)) # error: [static-assert-error]
1484+
static_assert(is_subtype_of(Callable[[int], str], Foo))
1485+
static_assert(is_assignable_to(Callable[[int], str], Foo))
14861486

1487-
static_assert(not is_subtype_of(Callable[[str], str], Foo))
1488-
static_assert(not is_assignable_to(Callable[[str], str], Foo))
1489-
static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo))
1490-
static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo))
1487+
# TODO: these should pass
1488+
static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error]
1489+
static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error]
1490+
static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error]
1491+
static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error]
14911492

14921493
def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]):
1493-
# TODO: this fails because we don't yet understand that all `Callable` types have a `__call__` method,
1494-
# and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass.
1495-
obj2 = obj # error: [invalid-assignment]
1494+
obj2 = obj
1495+
1496+
# TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable
1497+
# to the declared type of `obj2`
1498+
obj2 = obj3
1499+
1500+
def satisfies_foo(x: int) -> str:
1501+
return "foo"
14961502

1497-
# This diagnostic is correct, however.
1498-
obj2 = obj3 # error: [invalid-assignment]
1503+
static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo))
1504+
static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo))
14991505
```
15001506

15011507
## Protocols are never singleton types, and are never single-valued types

crates/red_knot_python_semantic/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2953,6 +2953,10 @@ impl<'db> Type<'db> {
29532953
Type::DataclassDecorator(_) => KnownClass::FunctionType
29542954
.to_instance(db)
29552955
.member_lookup_with_policy(db, name, policy),
2956+
2957+
Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => {
2958+
Symbol::bound(self).into()
2959+
}
29562960
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object
29572961
.to_instance(db)
29582962
.member_lookup_with_policy(db, name, policy),

0 commit comments

Comments
 (0)