Skip to content
Closed
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 @@ -672,6 +672,74 @@
c1: Callable[[int], None] = partial(f, y="a")
```

### Classes with `__call__` defined as attribute

```toml
[environment]
python-version = "3.11"
```

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

class A:
def method1(self, a: int) -> int:
return a

def __init__(self):
self.__call__ = self.method1

class B:
def method1(self, b: int) -> int:
return b
__call__ = method1

class C:
def method1(self, c: int) -> int:
return c
__call__: Callable[[Self, int], int] = method1

class D:
__call__: Callable[[Self, int], int] = lambda self, d: d

static_assert(not is_assignable_to(A, Callable[[int], int]))
static_assert(not is_assignable_to(A, Callable[[int, int], int]))
static_assert(not is_assignable_to(A, Callable[[int], str]))
static_assert(not is_assignable_to(A, Callable[[str], int]))
static_assert(not is_assignable_to(A, Callable[[str], str]))

static_assert(is_assignable_to(B, Callable[[int], int]))
static_assert(not is_assignable_to(B, Callable[[int, int], int]))
static_assert(not is_assignable_to(B, Callable[[int], str]))
static_assert(not is_assignable_to(B, Callable[[str], int]))
static_assert(not is_assignable_to(B, Callable[[str], str]))

static_assert(is_assignable_to(C, Callable[[int], int]))

Check failure on line 718 in crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(not is_assignable_to(C, Callable[[C, int], int]))
static_assert(not is_assignable_to(C, Callable[[int], str]))
static_assert(not is_assignable_to(C, Callable[[str], int]))
static_assert(not is_assignable_to(C, Callable[[str], str]))

static_assert(is_assignable_to(D, Callable[[int], int]))

Check failure on line 724 in crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(not is_assignable_to(D, Callable[[D, int], int]))
static_assert(not is_assignable_to(D, Callable[[int], str]))
static_assert(not is_assignable_to(D, Callable[[str], int]))
static_assert(not is_assignable_to(D, Callable[[str], str]))

a = A()
b = B()
c = C()
d = D()

def f(fn: Callable[[int], int]) -> None: ...

f(a) # error: [invalid-argument-type]
f(b)
f(c)

Check failure on line 739 in crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `(int, /) -> int`, found `C`"
f(d)

Check failure on line 740 in crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `(int, /) -> int`, found `D`"
```

## 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,63 @@
f(a)
```

### Classes with `__call__` defined as attribute

```toml
[environment]
python-version = "3.11"
```

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

class A:
def method1(self, a: int) -> int:
return a

def __init__(self):
self.__call__ = self.method1

class B:
def method1(self, b: int) -> int:
return b
__call__ = method1

class C:
def method1(self, c: int) -> int:
return c
__call__: Callable[[Self, int], int] = method1

class D:
__call__: Callable[[Self, int], int] = lambda self, d: d

static_assert(not is_subtype_of(A, Callable[[int], int]))
static_assert(not is_subtype_of(A, Callable[[int, int], int]))
static_assert(not is_subtype_of(A, Callable[[int], str]))
static_assert(not is_subtype_of(A, Callable[[str], int]))
static_assert(not is_subtype_of(A, Callable[[str], str]))

# __call__ is not declared, so we infer a union with Unknown, which is not fully static
static_assert(not is_subtype_of(B, Callable[[int], int]))
static_assert(not is_subtype_of(B, Callable[[int, int], int]))
static_assert(not is_subtype_of(B, Callable[[int], str]))
static_assert(not is_subtype_of(B, Callable[[str], int]))
static_assert(not is_subtype_of(B, Callable[[str], str]))

static_assert(is_subtype_of(C, Callable[[int], int]))

Check failure on line 1204 in crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(not is_subtype_of(C, Callable[[C, int], int]))
static_assert(not is_subtype_of(C, Callable[[int], str]))
static_assert(not is_subtype_of(C, Callable[[str], int]))
static_assert(not is_subtype_of(C, Callable[[str], str]))

static_assert(is_subtype_of(D, Callable[[int], int]))

Check failure on line 1210 in crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md

View workflow job for this annotation

GitHub Actions / cargo test (linux)

unexpected error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(not is_subtype_of(D, Callable[[D, int], int]))
static_assert(not is_subtype_of(D, Callable[[int], str]))
static_assert(not is_subtype_of(D, Callable[[str], int]))
static_assert(not is_subtype_of(D, Callable[[str], str]))
```

### Class literals

#### Classes with metaclasses
Expand Down
32 changes: 24 additions & 8 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,13 @@ impl<'db> Type<'db> {
matches!(self, Type::Callable(..))
}

pub const fn into_callable_type(self) -> Option<CallableType<'db>> {
match self {
Type::Callable(callable_type) => Some(callable_type),
_ => None,
}
}

fn is_none(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance()
.is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType))
Expand Down Expand Up @@ -1291,11 +1298,15 @@ impl<'db> Type<'db> {
}

(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self.member(db, "__call__").symbol;
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.symbol;
match call_symbol {
Symbol::Type(Type::BoundMethod(call_function), _) => call_function
.into_callable_type(db)
.is_subtype_of(db, target),
Symbol::Type(t, Boundness::Bound) => t.is_subtype_of(db, target),
_ => false,
}
}
Expand Down Expand Up @@ -1641,11 +1652,15 @@ impl<'db> Type<'db> {
}

(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self.member(db, "__call__").symbol;
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.symbol;
match call_symbol {
Symbol::Type(Type::BoundMethod(call_function), _) => call_function
.into_callable_type(db)
.is_assignable_to(db, target),
Symbol::Type(t, Boundness::Bound) => t.is_assignable_to(db, target),
_ => false,
}
}
Expand Down Expand Up @@ -2746,6 +2761,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