[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:
Charlie Marsh
2026-01-05 22:47:03 -05:00
committed by GitHub
parent a10e42294b
commit 28fa02129b
7 changed files with 324 additions and 7 deletions

View File

@@ -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),
)));
}
}

View File

@@ -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 =

View File

@@ -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)

View File

@@ -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,
)),
};