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",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
13030,
|
||||
13100,
|
||||
);
|
||||
|
||||
static TANJUN: Benchmark = Benchmark::new(
|
||||
|
|
|
|||
|
|
@ -113,9 +113,7 @@ intention that it shouldn't influence the method's descriptor behavior. For exam
|
|||
```py
|
||||
from typing import Callable
|
||||
|
||||
# TODO: this could use a generic signature, but we don't support
|
||||
# `ParamSpec` and solving of typevars inside `Callable` types yet.
|
||||
def memoize(f: Callable[[C1, int], str]) -> Callable[[C1, int], str]:
|
||||
def memoize[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
raise NotImplementedError
|
||||
|
||||
class C1:
|
||||
|
|
@ -259,4 +257,37 @@ reveal_type(MyClass().method) # revealed: (...) -> int
|
|||
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
|
||||
|
|
|
|||
|
|
@ -444,20 +444,18 @@ When a `@classmethod` is additionally decorated with another decorator, it is st
|
|||
class method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def does_nothing[T](f: T) -> T:
|
||||
return f
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
def f1(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
def f2(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: str
|
||||
|
|
|
|||
|
|
@ -4601,15 +4601,18 @@ impl<'db> Type<'db> {
|
|||
owner.display(db)
|
||||
);
|
||||
match self {
|
||||
Type::Callable(callable) if callable.is_function_like(db) => {
|
||||
// For "function-like" callables, model the behavior of `FunctionType.__get__`.
|
||||
Type::Callable(callable)
|
||||
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,
|
||||
// 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
|
||||
// `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.
|
||||
return if instance.is_none(db) {
|
||||
return if instance.is_none(db) && callable.is_function_like(db) {
|
||||
Some((self, AttributeKind::NormalOrNonDataDescriptor))
|
||||
} else {
|
||||
Some((
|
||||
|
|
@ -12243,6 +12246,10 @@ pub enum CallableTypeKind {
|
|||
/// instances, i.e. they bind `self`.
|
||||
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.
|
||||
ParamSpecValue,
|
||||
}
|
||||
|
|
@ -12339,6 +12346,10 @@ impl<'db> CallableType<'db> {
|
|||
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(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
|||
|
|
@ -1087,7 +1087,12 @@ impl<'db> FunctionType<'db> {
|
|||
|
||||
/// Convert the `FunctionType` into a [`CallableType`].
|
||||
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`].
|
||||
|
|
|
|||
|
|
@ -2408,37 +2408,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
.map(|bindings| bindings.return_type(self.db()))
|
||||
{
|
||||
Ok(return_ty) => {
|
||||
fn into_function_like_callable<'d>(
|
||||
fn propagate_callable_kind<'d>(
|
||||
db: &'d dyn Db,
|
||||
ty: Type<'d>,
|
||||
kind: CallableTypeKind,
|
||||
) -> Option<Type<'d>> {
|
||||
match ty {
|
||||
Type::Callable(callable) => Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
callable.signatures(db),
|
||||
CallableTypeKind::FunctionLike,
|
||||
kind,
|
||||
))),
|
||||
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
|
||||
// the decorator to be explicitly annotated as returning an intersection.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let is_input_function_like = inferred_ty
|
||||
let propagatable_kind = inferred_ty
|
||||
.try_upcast_to_callable(self.db())
|
||||
.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
|
||||
&& let Some(return_ty_function_like) =
|
||||
into_function_like_callable(self.db(), return_ty)
|
||||
if let Some(return_ty_modified) = propagatable_kind
|
||||
.and_then(|kind| propagate_callable_kind(self.db(), return_ty, kind))
|
||||
{
|
||||
// When a method on a class is decorated with a function that returns a `Callable`, assume that
|
||||
// the returned callable is also function-like. See "Decorating a method with a `Callable`-typed
|
||||
// decorator" in `callables_as_descriptors.md` for the extended explanation.
|
||||
return_ty_function_like
|
||||
// When a method on a class is decorated with a function that returns a
|
||||
// `Callable`, assume that the returned callable is also function-like (or
|
||||
// classmethod-like). See "Decorating a method with a `Callable`-typed
|
||||
// decorator" in `callables_as_descriptors.md` for the extended
|
||||
// explanation.
|
||||
return_ty_modified
|
||||
} else {
|
||||
return_ty
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue