[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

@@ -0,0 +1,246 @@
# `functools.total_ordering`
The `@functools.total_ordering` decorator allows a class to define a single comparison method (like
`__lt__`), and the decorator automatically generates the remaining comparison methods (`__le__`,
`__gt__`, `__ge__`). Defining `__eq__` is optional, as it can be inherited from `object`.
## Basic usage
When a class defines `__eq__` and `__lt__`, the decorator synthesizes `__le__`, `__gt__`, and
`__ge__`:
```py
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, grade: int):
self.grade = grade
def __eq__(self, other: object) -> bool:
if not isinstance(other, Student):
return NotImplemented
return self.grade == other.grade
def __lt__(self, other: "Student") -> bool:
return self.grade < other.grade
s1 = Student(85)
s2 = Student(90)
# User-defined comparison methods work as expected.
reveal_type(s1 == s2) # revealed: bool
reveal_type(s1 < s2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(s1 <= s2) # revealed: bool
reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Using `__gt__` as the root comparison method
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
`__ge__`:
```py
from functools import total_ordering
@total_ordering
class Priority:
def __init__(self, level: int):
self.level = level
def __eq__(self, other: object) -> bool:
if not isinstance(other, Priority):
return NotImplemented
return self.level == other.level
def __gt__(self, other: "Priority") -> bool:
return self.level > other.level
p1 = Priority(1)
p2 = Priority(2)
# User-defined comparison methods work
reveal_type(p1 == p2) # revealed: bool
reveal_type(p1 > p2) # revealed: bool
# Synthesized comparison methods are available
reveal_type(p1 < p2) # revealed: bool
reveal_type(p1 <= p2) # revealed: bool
reveal_type(p1 >= p2) # revealed: bool
```
## Inherited `__eq__`
A class only needs to define a single comparison method. The `__eq__` method can be inherited from
`object`:
```py
from functools import total_ordering
@total_ordering
class Score:
def __init__(self, value: int):
self.value = value
def __lt__(self, other: "Score") -> bool:
return self.value < other.value
s1 = Score(85)
s2 = Score(90)
# `__eq__` is inherited from object.
reveal_type(s1 == s2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(s1 <= s2) # revealed: bool
reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Inherited ordering methods
The decorator also works when the ordering method is inherited from a superclass:
```py
from functools import total_ordering
class Base:
def __lt__(self, other: "Base") -> bool:
return True
@total_ordering
class Child(Base):
def __eq__(self, other: object) -> bool:
if not isinstance(other, Child):
return NotImplemented
return True
c1 = Child()
c2 = Child()
# Synthesized methods work even though `__lt__` is inherited.
reveal_type(c1 <= c2) # revealed: bool
reveal_type(c1 > c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Explicitly-defined methods are not overridden
When a class explicitly defines multiple comparison methods, the decorator does not override them.
We use a narrower return type (`Literal[True]`) to verify that the explicit methods are preserved:
```py
from functools import total_ordering
from typing import Literal
@total_ordering
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius
def __lt__(self, other: "Temperature") -> Literal[True]:
return True
def __gt__(self, other: "Temperature") -> Literal[True]:
return True
t1 = Temperature(20.0)
t2 = Temperature(25.0)
# User-defined methods preserve their return type.
reveal_type(t1 < t2) # revealed: Literal[True]
reveal_type(t1 > t2) # revealed: Literal[True]
# Synthesized methods have `bool` return type.
reveal_type(t1 <= t2) # revealed: bool
reveal_type(t1 >= t2) # revealed: bool
```
## Combined with `@dataclass`
The decorator works with `@dataclass`:
```py
from dataclasses import dataclass
from functools import total_ordering
@total_ordering
@dataclass
class Point:
x: int
y: int
def __lt__(self, other: "Point") -> bool:
return (self.x, self.y) < (other.x, other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
# Dataclass-synthesized `__eq__` is available.
reveal_type(p1 == p2) # revealed: bool
# User-defined comparison method works.
reveal_type(p1 < p2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(p1 <= p2) # revealed: bool
reveal_type(p1 > p2) # revealed: bool
reveal_type(p1 >= p2) # revealed: bool
```
## Missing ordering method
If a class has `@total_ordering` but doesn't define any ordering method (itself or in a superclass),
the decorator would fail at runtime. We don't synthesize methods in this case:
```py
from functools import total_ordering
@total_ordering
class NoOrdering:
def __eq__(self, other: object) -> bool:
return True
n1 = NoOrdering()
n2 = NoOrdering()
# These should error because no ordering method is defined.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
## Without the decorator
Without `@total_ordering`, classes that only define `__lt__` will not have `__le__` or `__ge__`
synthesized:
```py
class NoDecorator:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, NoDecorator):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "NoDecorator") -> bool:
return self.value < other.value
n1 = NoDecorator(1)
n2 = NoDecorator(2)
# User-defined methods work.
reveal_type(n1 == n2) # revealed: bool
reveal_type(n1 < n2) # revealed: bool
# Note: `n1 > n2` works because Python reflects it to `n2 < n1`
reveal_type(n1 > n2) # revealed: bool
# These comparison operators are not available.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```