mirror of https://github.com/astral-sh/ruff
[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:
parent
c4a08782cc
commit
52b0470870
|
|
@ -300,12 +300,7 @@ from typing import Callable
|
||||||
def _(c: Callable[[int], int]):
|
def _(c: Callable[[int], int]):
|
||||||
reveal_type(c.__init__) # revealed: def __init__(self) -> None
|
reveal_type(c.__init__) # revealed: def __init__(self) -> None
|
||||||
reveal_type(c.__class__) # revealed: type
|
reveal_type(c.__class__) # revealed: type
|
||||||
|
reveal_type(c.__call__) # revealed: (int, /) -> int
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
||||||
|
|
|
||||||
|
|
@ -1476,26 +1476,32 @@ signature implied by the `Callable` type is assignable to the signature of the `
|
||||||
specified by the protocol:
|
specified by the protocol:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
from knot_extensions import TypeOf
|
||||||
|
|
||||||
class Foo(Protocol):
|
class Foo(Protocol):
|
||||||
def __call__(self, x: int, /) -> str: ...
|
def __call__(self, x: int, /) -> str: ...
|
||||||
|
|
||||||
# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method,
|
static_assert(is_subtype_of(Callable[[int], str], Foo))
|
||||||
# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass.
|
static_assert(is_assignable_to(Callable[[int], str], Foo))
|
||||||
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(not is_subtype_of(Callable[[str], str], Foo))
|
# TODO: these should pass
|
||||||
static_assert(not is_assignable_to(Callable[[str], str], Foo))
|
static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error]
|
||||||
static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo))
|
static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error]
|
||||||
static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo))
|
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]):
|
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,
|
obj2 = obj
|
||||||
# and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass.
|
|
||||||
obj2 = obj # error: [invalid-assignment]
|
|
||||||
|
|
||||||
# This diagnostic is correct, however.
|
# TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable
|
||||||
obj2 = obj3 # error: [invalid-assignment]
|
# 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
|
## Protocols are never singleton types, and are never single-valued types
|
||||||
|
|
|
||||||
|
|
@ -2953,6 +2953,10 @@ impl<'db> Type<'db> {
|
||||||
Type::DataclassDecorator(_) => KnownClass::FunctionType
|
Type::DataclassDecorator(_) => KnownClass::FunctionType
|
||||||
.to_instance(db)
|
.to_instance(db)
|
||||||
.member_lookup_with_policy(db, name, policy),
|
.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
|
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object
|
||||||
.to_instance(db)
|
.to_instance(db)
|
||||||
.member_lookup_with_policy(db, name, policy),
|
.member_lookup_with_policy(db, name, policy),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue