[ty] Add a diagnostic for @functools.total_ordering without a defined comparison method (#22183)

## Summary

This raises a `ValueError` at runtime:

```python
from functools import total_ordering

@total_ordering
class NoOrdering:
    def __eq__(self, other: object) -> bool:
        return True
```

Specifically:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/functools.py", line 193, in total_ordering
    raise ValueError('must define at least one ordering operation: < > <= >=')
ValueError: must define at least one ordering operation: < > <= >=
```

See: https://github.com/astral-sh/ty/issues/1202.
This commit is contained in:
Charlie Marsh
2026-01-05 23:14:06 -05:00
committed by GitHub
parent 28fa02129b
commit 8b8b174e4f
7 changed files with 251 additions and 85 deletions

View File

@@ -583,7 +583,7 @@ from module import NotFrozenBase
@final
@dataclass(frozen=True)
@total_ordering
@total_ordering # error: [invalid-total-ordering]
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
```

View File

@@ -194,12 +194,12 @@ 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:
a diagnostic is emitted at the decorator site:
```py
from functools import total_ordering
@total_ordering
@total_ordering # error: [invalid-total-ordering]
class NoOrdering:
def __eq__(self, other: object) -> bool:
return True
@@ -207,7 +207,7 @@ class NoOrdering:
n1 = NoOrdering()
n2 = NoOrdering()
# These should error because no ordering method is defined.
# Comparison operators also error because no methods were synthesized.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```

View File

@@ -61,7 +61,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
6 |
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering
9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
```
@@ -126,6 +126,22 @@ info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```
```
error[invalid-total-ordering]: Class decorated with `@total_ordering` must define at least one ordering method
--> src/main.py:9:1
|
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering # error: [invalid-total-ordering]
| ^^^^^^^^^^^^^^^ `FrozenChild` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
|
info: The decorator will raise `ValueError` at runtime
info: rule `invalid-total-ordering` is enabled by default
```
```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/main.py:8:1
@@ -133,7 +149,7 @@ error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from n
7 | @final
8 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
9 | @total_ordering
9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
11 | y: str