[ty] propagate classmethod-ness through decorators returning Callables (#21958)
Fixes https://github.com/astral-sh/ty/issues/1787 ## Summary Allow method decorators returning Callables to presumptively propagate "classmethod-ness" in the same way that they already presumptively propagate "function-like-ness". We can't actually be sure that this is the case, based on the decorator's annotations, but (along with other type checkers) we heuristically assume it to be the case for decorators applied via decorator syntax. ## Test Plan Added mdtest.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user