Compare commits

...

2 Commits

Author SHA1 Message Date
David Peter
697e6ccbd3 [ty] Treat ClassVar-qualified Callables as bound-method descriptors 2025-10-14 13:51:16 +02:00
David Peter
70378c9acd [ty] Treat Callable dunder members as bound method descriptors 2025-10-14 13:43:19 +02:00
5 changed files with 82 additions and 11 deletions

View File

@@ -204,3 +204,37 @@ class Calculator:
reveal_type(Calculator().square_then_round(3.14)) # revealed: Unknown | int
```
## Use case: Treating dunder methods as bound-method descriptors
pytorch defines a `__pow__` dunder attribute on [`TensorBase`] in a similar way to the following
example. We generally treat dunder attributes as bound-method descriptors since they all take a
`self` argument. This allows us to type-check the following code correctly:
```py
from typing import Callable
def pow_impl(tensor: Tensor, exponent: int) -> Tensor:
raise NotImplementedError
class Tensor:
__pow__: Callable[[Tensor, int], Tensor] = pow_impl
Tensor() ** 2
```
The following example is also taken from a real world project. Here, the `__lt__` dunder attribute
is not declared. The attribute type is therefore inferred as `Unknown | Callable[…]`, but we still
treat it as a bound-method descriptor:
```py
def make_comparison_operator(name: str) -> Callable[[Matrix, Matrix], bool]:
raise NotImplementedError
class Matrix:
__lt__ = make_comparison_operator("lt")
Matrix() < Matrix()
```
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092

View File

@@ -1181,18 +1181,17 @@ static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [s
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 __future__ import annotations
from typing import Callable
from ty_extensions import static_assert, is_assignable_to
def call_impl(a: int) -> str:
def call_impl(a: A, x: int) -> str:
return ""
class A:
__call__: Callable[[int], str] = call_impl
__call__: Callable[[A, int], str] = call_impl
static_assert(is_assignable_to(A, Callable[[int], str]))
static_assert(not is_assignable_to(A, Callable[[int], int]))

View File

@@ -1635,18 +1635,17 @@ f(a)
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 __future__ import annotations
from typing import Callable
from ty_extensions import static_assert, is_subtype_of
def call_impl(a: int) -> str:
def call_impl(a: A, x: int) -> str:
return ""
class A:
__call__: Callable[[int], str] = call_impl
__call__: Callable[[A, int], str] = call_impl
static_assert(is_subtype_of(A, Callable[[int], str]))
static_assert(not is_subtype_of(A, Callable[[int], int]))

View File

@@ -11116,6 +11116,23 @@ impl<'db> IntersectionType<'db> {
}
}
/// Map a type transformation over all positive elements of the intersection. Leave the
/// negative elements unchanged.
pub(crate) fn map_positive(
self,
db: &'db dyn Db,
mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>,
) -> Type<'db> {
let mut builder = IntersectionBuilder::new(db);
for ty in self.positive(db) {
builder = builder.add_positive(transform_fn(ty));
}
for ty in self.negative(db) {
builder = builder.add_negative(*ty);
}
builder.build()
}
pub(crate) fn map_with_boundness(
self,
db: &'db dyn Db,

View File

@@ -2014,7 +2014,29 @@ impl<'db> ClassLiteral<'db> {
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
self.class_member_inner(db, None, name, policy)
fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> {
match ty {
Type::Callable(callable_ty) => {
Type::Callable(CallableType::new(db, callable_ty.signatures(db), true))
}
Type::Union(union) => {
union.map(db, |element| into_function_like_callable(db, *element))
}
Type::Intersection(intersection) => intersection
.map_positive(db, |element| into_function_like_callable(db, *element)),
_ => ty,
}
}
let mut member = self.class_member_inner(db, None, name, policy);
// We generally treat dunder attributes with `Callable` types as function-like callables.
// See `callables_as_descriptors.md` for more details.
if (name.starts_with("__") && name.ends_with("__")) || member.is_class_var() {
member = member.map_type(|ty| into_function_like_callable(db, ty));
}
member
}
fn class_member_inner(