mirror of https://github.com/astral-sh/ruff
[ty] propagate classmethod-ness through decorators returning Callables
This commit is contained in:
parent
7f7485d608
commit
88c12ca7ee
|
|
@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
|
||||||
max_dep_date: "2025-06-17",
|
max_dep_date: "2025-06-17",
|
||||||
python_version: PythonVersion::PY312,
|
python_version: PythonVersion::PY312,
|
||||||
},
|
},
|
||||||
13030,
|
13100,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TANJUN: Benchmark = Benchmark::new(
|
static TANJUN: Benchmark = Benchmark::new(
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,7 @@ intention that it shouldn't influence the method's descriptor behavior. For exam
|
||||||
```py
|
```py
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
# TODO: this could use a generic signature, but we don't support
|
def memoize[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||||
# `ParamSpec` and solving of typevars inside `Callable` types yet.
|
|
||||||
def memoize(f: Callable[[C1, int], str]) -> Callable[[C1, int], str]:
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
class C1:
|
class C1:
|
||||||
|
|
@ -259,4 +257,37 @@ reveal_type(MyClass().method) # revealed: (...) -> int
|
||||||
reveal_type(MyClass().method.__name__) # revealed: str
|
reveal_type(MyClass().method.__name__) # revealed: str
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## classmethods passed through Callable-returning decorators
|
||||||
|
|
||||||
|
The behavior described above is also applied to classmethods. If a method is decorated with
|
||||||
|
`@classmethod`, and also with another decorator which returns a Callable type, we make the
|
||||||
|
assumption that the decorator returns a callable which still has the classmethod descriptor
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||||
|
return func
|
||||||
|
|
||||||
|
class C:
|
||||||
|
@callable_identity
|
||||||
|
@classmethod
|
||||||
|
def f1(cls, x: int) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@callable_identity
|
||||||
|
def f2(cls, x: int) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
C.f1(C, 1)
|
||||||
|
C.f1(1)
|
||||||
|
C().f1(1)
|
||||||
|
C.f2(1)
|
||||||
|
C().f2(1)
|
||||||
|
```
|
||||||
|
|
||||||
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
||||||
|
|
|
||||||
|
|
@ -444,20 +444,18 @@ When a `@classmethod` is additionally decorated with another decorator, it is st
|
||||||
class method:
|
class method:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
def does_nothing[T](f: T) -> T:
|
def does_nothing[T](f: T) -> T:
|
||||||
return f
|
return f
|
||||||
|
|
||||||
class C:
|
class C:
|
||||||
@classmethod
|
@classmethod
|
||||||
@does_nothing
|
@does_nothing
|
||||||
def f1(cls: type[C], x: int) -> str:
|
def f1(cls, x: int) -> str:
|
||||||
return "a"
|
return "a"
|
||||||
|
|
||||||
@does_nothing
|
@does_nothing
|
||||||
@classmethod
|
@classmethod
|
||||||
def f2(cls: type[C], x: int) -> str:
|
def f2(cls, x: int) -> str:
|
||||||
return "a"
|
return "a"
|
||||||
|
|
||||||
reveal_type(C.f1(1)) # revealed: str
|
reveal_type(C.f1(1)) # revealed: str
|
||||||
|
|
|
||||||
|
|
@ -4601,15 +4601,18 @@ impl<'db> Type<'db> {
|
||||||
owner.display(db)
|
owner.display(db)
|
||||||
);
|
);
|
||||||
match self {
|
match self {
|
||||||
Type::Callable(callable) if callable.is_function_like(db) => {
|
Type::Callable(callable)
|
||||||
// For "function-like" callables, model the behavior of `FunctionType.__get__`.
|
if callable.is_function_like(db) || callable.is_classmethod_like(db) =>
|
||||||
|
{
|
||||||
|
// For "function-like" or "classmethod-like" callables, model the behavior of
|
||||||
|
// `FunctionType.__get__` or `classmethod.__get__`.
|
||||||
//
|
//
|
||||||
// It is a shortcut to model this in `try_call_dunder_get`. If we want to be really precise,
|
// It is a shortcut to model this in `try_call_dunder_get`. If we want to be really precise,
|
||||||
// we should instead return a new method-wrapper type variant for the synthesized `__get__`
|
// we should instead return a new method-wrapper type variant for the synthesized `__get__`
|
||||||
// method of these synthesized functions. The method-wrapper would then be returned from
|
// method of these synthesized functions. The method-wrapper would then be returned from
|
||||||
// `find_name_in_mro` when called on function-like `Callable`s. This would allow us to
|
// `find_name_in_mro` when called on function-like `Callable`s. This would allow us to
|
||||||
// correctly model the behavior of *explicit* `SomeDataclass.__init__.__get__` calls.
|
// correctly model the behavior of *explicit* `SomeDataclass.__init__.__get__` calls.
|
||||||
return if instance.is_none(db) {
|
return if instance.is_none(db) && callable.is_function_like(db) {
|
||||||
Some((self, AttributeKind::NormalOrNonDataDescriptor))
|
Some((self, AttributeKind::NormalOrNonDataDescriptor))
|
||||||
} else {
|
} else {
|
||||||
Some((
|
Some((
|
||||||
|
|
@ -12243,6 +12246,10 @@ pub enum CallableTypeKind {
|
||||||
/// instances, i.e. they bind `self`.
|
/// instances, i.e. they bind `self`.
|
||||||
FunctionLike,
|
FunctionLike,
|
||||||
|
|
||||||
|
/// A callable type that we believe represents a classmethod (i.e. it will unconditionally bind
|
||||||
|
/// the first argument on `__get__`).
|
||||||
|
ClassMethodLike,
|
||||||
|
|
||||||
/// Represents the value bound to a `typing.ParamSpec` type variable.
|
/// Represents the value bound to a `typing.ParamSpec` type variable.
|
||||||
ParamSpecValue,
|
ParamSpecValue,
|
||||||
}
|
}
|
||||||
|
|
@ -12339,6 +12346,10 @@ impl<'db> CallableType<'db> {
|
||||||
matches!(self.kind(db), CallableTypeKind::FunctionLike)
|
matches!(self.kind(db), CallableTypeKind::FunctionLike)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_classmethod_like(self, db: &'db dyn Db) -> bool {
|
||||||
|
matches!(self.kind(db), CallableTypeKind::ClassMethodLike)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn bind_self(
|
pub(crate) fn bind_self(
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
|
|
|
||||||
|
|
@ -1087,7 +1087,12 @@ impl<'db> FunctionType<'db> {
|
||||||
|
|
||||||
/// Convert the `FunctionType` into a [`CallableType`].
|
/// Convert the `FunctionType` into a [`CallableType`].
|
||||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
||||||
CallableType::new(db, self.signature(db), CallableTypeKind::FunctionLike)
|
let kind = if self.is_classmethod(db) {
|
||||||
|
CallableTypeKind::ClassMethodLike
|
||||||
|
} else {
|
||||||
|
CallableTypeKind::FunctionLike
|
||||||
|
};
|
||||||
|
CallableType::new(db, self.signature(db), kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the `FunctionType` into a [`BoundMethodType`].
|
/// Convert the `FunctionType` into a [`BoundMethodType`].
|
||||||
|
|
|
||||||
|
|
@ -2408,37 +2408,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
.map(|bindings| bindings.return_type(self.db()))
|
.map(|bindings| bindings.return_type(self.db()))
|
||||||
{
|
{
|
||||||
Ok(return_ty) => {
|
Ok(return_ty) => {
|
||||||
fn into_function_like_callable<'d>(
|
fn propagate_callable_kind<'d>(
|
||||||
db: &'d dyn Db,
|
db: &'d dyn Db,
|
||||||
ty: Type<'d>,
|
ty: Type<'d>,
|
||||||
|
kind: CallableTypeKind,
|
||||||
) -> Option<Type<'d>> {
|
) -> Option<Type<'d>> {
|
||||||
match ty {
|
match ty {
|
||||||
Type::Callable(callable) => Some(Type::Callable(CallableType::new(
|
Type::Callable(callable) => Some(Type::Callable(CallableType::new(
|
||||||
db,
|
db,
|
||||||
callable.signatures(db),
|
callable.signatures(db),
|
||||||
CallableTypeKind::FunctionLike,
|
kind,
|
||||||
))),
|
))),
|
||||||
Type::Union(union) => union
|
Type::Union(union) => union
|
||||||
.try_map(db, |element| into_function_like_callable(db, *element)),
|
.try_map(db, |element| propagate_callable_kind(db, *element, kind)),
|
||||||
// Intersections are currently not handled here because that would require
|
// Intersections are currently not handled here because that would require
|
||||||
// the decorator to be explicitly annotated as returning an intersection.
|
// the decorator to be explicitly annotated as returning an intersection.
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_input_function_like = inferred_ty
|
let propagatable_kind = inferred_ty
|
||||||
.try_upcast_to_callable(self.db())
|
.try_upcast_to_callable(self.db())
|
||||||
.and_then(CallableTypes::exactly_one)
|
.and_then(CallableTypes::exactly_one)
|
||||||
.is_some_and(|callable| callable.is_function_like(self.db()));
|
.and_then(|callable| match callable.kind(self.db()) {
|
||||||
|
kind @ (CallableTypeKind::FunctionLike
|
||||||
|
| CallableTypeKind::ClassMethodLike) => Some(kind),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
if is_input_function_like
|
if let Some(return_ty_modified) = propagatable_kind
|
||||||
&& let Some(return_ty_function_like) =
|
.and_then(|kind| propagate_callable_kind(self.db(), return_ty, kind))
|
||||||
into_function_like_callable(self.db(), return_ty)
|
|
||||||
{
|
{
|
||||||
// When a method on a class is decorated with a function that returns a `Callable`, assume that
|
// When a method on a class is decorated with a function that returns a
|
||||||
// the returned callable is also function-like. See "Decorating a method with a `Callable`-typed
|
// `Callable`, assume that the returned callable is also function-like (or
|
||||||
// decorator" in `callables_as_descriptors.md` for the extended explanation.
|
// classmethod-like). See "Decorating a method with a `Callable`-typed
|
||||||
return_ty_function_like
|
// decorator" in `callables_as_descriptors.md` for the extended
|
||||||
|
// explanation.
|
||||||
|
return_ty_modified
|
||||||
} else {
|
} else {
|
||||||
return_ty
|
return_ty
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue