Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 19 additions & 13 deletions crates/red_knot_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

# 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"

# This diagnostic is correct, however.
obj2 = obj3 # error: [invalid-assignment]
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
Expand Down
4 changes: 4 additions & 0 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading