mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 21:40:51 -05:00
[ty] Add support for @total_ordering (#22181)
## Summary We have some suppressions in the pyx codebase related to this, so wanted to resolve. Closes https://github.com/astral-sh/ty/issues/1202.
This commit is contained in:
@@ -1122,6 +1122,7 @@ impl<'db> Bindings<'db> {
|
||||
class_literal.type_check_only(db),
|
||||
Some(params),
|
||||
class_literal.dataclass_transformer_params(db),
|
||||
class_literal.total_ordering(db),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,6 +1516,9 @@ pub struct ClassLiteral<'db> {
|
||||
|
||||
pub(crate) dataclass_params: Option<DataclassParams<'db>>,
|
||||
pub(crate) dataclass_transformer_params: Option<DataclassTransformerParams<'db>>,
|
||||
|
||||
/// Whether this class is decorated with `@functools.total_ordering`
|
||||
pub(crate) total_ordering: bool,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
@@ -1540,6 +1543,17 @@ impl<'db> ClassLiteral<'db> {
|
||||
self.is_known(db, KnownClass::Tuple)
|
||||
}
|
||||
|
||||
/// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`,
|
||||
/// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if
|
||||
/// synthesis is valid.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool {
|
||||
let body_scope = self.body_scope(db);
|
||||
["__lt__", "__le__", "__gt__", "__ge__"]
|
||||
.iter()
|
||||
.any(|method| !class_member(db, body_scope, method).is_undefined())
|
||||
}
|
||||
|
||||
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
|
||||
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
|
||||
// the knowledge that this class is not generic.
|
||||
@@ -2384,6 +2398,41 @@ impl<'db> ClassLiteral<'db> {
|
||||
) -> Option<Type<'db>> {
|
||||
let dataclass_params = self.dataclass_params(db);
|
||||
|
||||
// Handle `@functools.total_ordering`: synthesize comparison methods
|
||||
// for classes that have `@total_ordering` and define at least one
|
||||
// ordering method. The decorator requires at least one of __lt__,
|
||||
// __le__, __gt__, or __ge__ to be defined (either in this class or
|
||||
// inherited from a superclass, excluding `object`).
|
||||
if self.total_ordering(db) && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
|
||||
// Check if any class in the MRO (excluding object) defines at least one
|
||||
// ordering method in its own body (not synthesized).
|
||||
let has_ordering_method = self
|
||||
.iter_mro(db, specialization)
|
||||
.filter_map(super::class_base::ClassBase::into_class)
|
||||
.filter(|class| !class.class_literal(db).0.is_known(db, KnownClass::Object))
|
||||
.any(|class| class.class_literal(db).0.has_own_ordering_method(db));
|
||||
|
||||
if has_ordering_method {
|
||||
let instance_ty =
|
||||
Type::instance(db, self.apply_optional_specialization(db, specialization));
|
||||
|
||||
let signature = Signature::new(
|
||||
Parameters::new(
|
||||
db,
|
||||
[
|
||||
Parameter::positional_or_keyword(Name::new_static("self"))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_or_keyword(Name::new_static("other"))
|
||||
.with_annotated_type(instance_ty),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
);
|
||||
|
||||
return Some(Type::function_like_callable(db, signature));
|
||||
}
|
||||
}
|
||||
|
||||
let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;
|
||||
|
||||
let mut transformer_params =
|
||||
|
||||
@@ -1413,6 +1413,9 @@ pub enum KnownFunction {
|
||||
/// `dataclasses.field`
|
||||
Field,
|
||||
|
||||
/// `functools.total_ordering`
|
||||
TotalOrdering,
|
||||
|
||||
/// `inspect.getattr_static`
|
||||
GetattrStatic,
|
||||
|
||||
@@ -1501,6 +1504,7 @@ impl KnownFunction {
|
||||
Self::Dataclass | Self::Field => {
|
||||
matches!(module, KnownModule::Dataclasses)
|
||||
}
|
||||
Self::TotalOrdering => module.is_functools(),
|
||||
Self::GetattrStatic => module.is_inspect(),
|
||||
Self::IsAssignableTo
|
||||
| Self::IsDisjointFrom
|
||||
@@ -2068,6 +2072,7 @@ pub(crate) mod tests {
|
||||
|
||||
KnownFunction::ImportModule => KnownModule::ImportLib,
|
||||
KnownFunction::NamedTuple => KnownModule::Collections,
|
||||
KnownFunction::TotalOrdering => KnownModule::Functools,
|
||||
};
|
||||
|
||||
let function_definition = known_module_symbol(&db, module, function_name)
|
||||
|
||||
@@ -2864,6 +2864,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let mut type_check_only = false;
|
||||
let mut dataclass_params = None;
|
||||
let mut dataclass_transformer_params = None;
|
||||
let mut total_ordering = false;
|
||||
for decorator in decorator_list {
|
||||
let decorator_ty = self.infer_decorator(decorator);
|
||||
if decorator_ty
|
||||
@@ -2874,6 +2875,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if decorator_ty
|
||||
.as_function_literal()
|
||||
.is_some_and(|function| function.is_known(self.db(), KnownFunction::TotalOrdering))
|
||||
{
|
||||
total_ordering = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Type::DataclassDecorator(params) = decorator_ty {
|
||||
dataclass_params = Some(params);
|
||||
continue;
|
||||
@@ -2961,6 +2970,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
type_check_only,
|
||||
dataclass_params,
|
||||
dataclass_transformer_params,
|
||||
total_ordering,
|
||||
)),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user