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
34 changes: 34 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/dunder.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,37 @@ def _(flag: bool):
# error: [possibly-unbound-implicit-call]
reveal_type(c[0]) # revealed: str
```

## Dunder methods cannot be looked up on instances

Class-level annotations with no value assigned are considered instance-only, and aren't available as
dunder methods:

```py
from typing import Callable

class C:
__call__: Callable[..., None]

# error: [call-non-callable]
C()()

# error: [invalid-assignment]
_: Callable[..., None] = C()
```

And of course the same is true if we have only an implicit assignment inside a method:

```py
from typing import Callable

class C:
def __init__(self):
self.__call__ = lambda *a, **kw: None

# error: [call-non-callable]
C()()

# error: [invalid-assignment]
_: Callable[..., None] = C()
```
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,29 @@ def f(x: int, y: str) -> None: ...
c1: Callable[[int], None] = partial(f, y="a")
```

### Classes with `__call__` as attribute

An instance type is assignable to a compatible callable type if the instance type's class has a
callable `__call__` attribute.

TODO: for the moment, we don't consider the callable type as a bound-method descriptor, but this may
change for better compatibility with mypy/pyright.

```py
from typing import Callable
from ty_extensions import static_assert, is_assignable_to

def call_impl(a: int) -> str:
return ""

class A:
__call__: Callable[[int], str] = call_impl

static_assert(is_assignable_to(A, Callable[[int], str]))
static_assert(not is_assignable_to(A, Callable[[int], int]))
reveal_type(A()(1)) # revealed: str
```

## Generics

### Assignability of generic types parameterized by gradual types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,29 @@ def f(fn: Callable[[int], int]) -> None: ...
f(a)
```

### Classes with `__call__` as attribute

An instance type can be a subtype of a compatible callable type if the instance type's class has a
callable `__call__` attribute.

TODO: for the moment, we don't consider the callable type as a bound-method descriptor, but this may
change for better compatibility with mypy/pyright.

```py
from typing import Callable
from ty_extensions import static_assert, is_subtype_of

def call_impl(a: int) -> str:
return ""

class A:
__call__: Callable[[int], str] = call_impl

static_assert(is_subtype_of(A, Callable[[int], str]))
static_assert(not is_subtype_of(A, Callable[[int], int]))
reveal_type(A()(1)) # revealed: str
```

### Class literals

#### Classes with metaclasses
Expand Down
41 changes: 29 additions & 12 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1291,12 +1291,20 @@ impl<'db> Type<'db> {
}

(Type::NominalInstance(_) | Type::ProtocolInstance(_), 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_subtype_of(db, target),
_ => false,
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.symbol;
// If the type of __call__ is a subtype of a callable type, this instance is.
// Don't add other special cases here; our subtyping of a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Symbol::Type(t, Boundness::Bound) = call_symbol {
t.is_subtype_of(db, target)
} else {
false
}
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
Expand Down Expand Up @@ -1641,12 +1649,20 @@ impl<'db> Type<'db> {
}

(Type::NominalInstance(_) | Type::ProtocolInstance(_), 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,
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.symbol;
// If the type of __call__ is assignable to a callable type, this instance is.
// Don't add other special cases here; our assignability to a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Symbol::Type(t, Boundness::Bound) = call_symbol {
t.is_assignable_to(db, target)
} else {
false
}
}

Expand Down Expand Up @@ -2746,6 +2762,7 @@ impl<'db> Type<'db> {
instance.display(db),
owner.display(db)
);

let descr_get = self.class_member(db, "__get__".into()).symbol;

if let Symbol::Type(descr_get, descr_get_boundness) = descr_get {
Expand Down
Loading