From 8b8b174e4f4be9a0dc7ec0bf451676dbbf65c57a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jan 2026 23:14:06 -0500 Subject: [PATCH] [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 "", line 1, in 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. --- crates/ty/docs/rules.md | 200 +++++++++++------- .../mdtest/dataclasses/dataclasses.md | 2 +- .../mdtest/decorators/total_ordering.md | 6 +- ...rozen__non-frozen_in…_(9af2ab07b8e829e).snap | 20 +- .../src/types/diagnostic.rs | 62 ++++++ .../src/types/infer/builder.rs | 36 +++- ty.schema.json | 10 + 7 files changed, 251 insertions(+), 85 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index ae4f3d5009..eb6d62eb85 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -80,7 +80,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -135,7 +135,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -167,7 +167,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -198,7 +198,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -262,7 +262,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -290,7 +290,7 @@ type B = A Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -346,7 +346,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -373,7 +373,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -529,7 +529,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -559,7 +559,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -674,7 +674,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -729,7 +729,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -799,7 +799,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -850,7 +850,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -879,7 +879,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -923,7 +923,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -965,7 +965,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1077,7 +1077,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1116,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1151,7 +1151,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1185,7 +1185,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1292,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1346,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1376,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1426,7 +1426,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1452,7 +1452,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1483,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1517,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1566,7 +1566,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1591,7 +1591,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1681,13 +1681,59 @@ class C: ... - [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations) - [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations) +## `invalid-total-ordering` + + +Default level: error · +Added in 0.0.10 · +Related issues · +View source + + + +**What it does** + +Checks for classes decorated with `@functools.total_ordering` that don't +define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`). + +**Why is this bad?** + +The `@total_ordering` decorator requires the class to define at least one +ordering method. If none is defined, Python raises a `ValueError` at runtime. + +**Example** + + +```python +from functools import total_ordering + +@total_ordering +class MyClass: # Error: no ordering method defined + def __eq__(self, other: object) -> bool: + return True +``` + +Use instead: + +```python +from functools import total_ordering + +@total_ordering +class MyClass: + def __eq__(self, other: object) -> bool: + return True + + def __lt__(self, other: "MyClass") -> bool: + return True +``` + ## `invalid-type-alias-type` Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1714,7 +1760,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1761,7 +1807,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1791,7 +1837,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1821,7 +1867,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1855,7 +1901,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1889,7 +1935,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1924,7 +1970,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -1955,7 +2001,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1980,7 +2026,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2013,7 +2059,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2042,7 +2088,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2068,7 +2114,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2092,7 +2138,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2125,7 +2171,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2152,7 +2198,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2179,7 +2225,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2207,7 +2253,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2239,7 +2285,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2276,7 +2322,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2340,7 +2386,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2367,7 +2413,7 @@ cast(int, f()) # Redundant Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2397,7 +2443,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2426,7 +2472,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2460,7 +2506,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2487,7 +2533,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2515,7 +2561,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2561,7 +2607,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2585,7 +2631,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2612,7 +2658,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2640,7 +2686,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2698,7 +2744,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2723,7 +2769,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2748,7 +2794,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2787,7 +2833,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2824,7 +2870,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2883,7 +2929,7 @@ a = 20 / 2 Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2946,7 +2992,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 94b7d45da3..e9e661c3a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md index 2739b31d61..48e6b7ee40 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md @@ -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] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap index fc2dc77a44..d21a71ca8d 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap @@ -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 diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a924e03d4a..f60cf81142 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -125,6 +125,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_EXPLICIT_OVERRIDE); registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD); registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS); + registry.register_lint(&INVALID_TOTAL_ORDERING); // String annotations registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); @@ -2329,6 +2330,46 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for classes decorated with `@functools.total_ordering` that don't + /// define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`). + /// + /// ## Why is this bad? + /// The `@total_ordering` decorator requires the class to define at least one + /// ordering method. If none is defined, Python raises a `ValueError` at runtime. + /// + /// ## Example + /// + /// ```python + /// from functools import total_ordering + /// + /// @total_ordering + /// class MyClass: # Error: no ordering method defined + /// def __eq__(self, other: object) -> bool: + /// return True + /// ``` + /// + /// Use instead: + /// + /// ```python + /// from functools import total_ordering + /// + /// @total_ordering + /// class MyClass: + /// def __eq__(self, other: object) -> bool: + /// return True + /// + /// def __lt__(self, other: "MyClass") -> bool: + /// return True + /// ``` + pub(crate) static INVALID_TOTAL_ORDERING = { + summary: "detects `@total_ordering` classes without an ordering method", + status: LintStatus::stable("0.0.10"), + default_level: Level::Error, + } +} + /// A collection of type check diagnostics. #[derive(Default, Eq, PartialEq, get_size2::GetSize)] pub struct TypeCheckDiagnostics { @@ -4618,6 +4659,27 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( } } +pub(super) fn report_invalid_total_ordering( + context: &InferContext<'_, '_>, + class: ClassLiteral<'_>, + decorator: &ast::Decorator, +) { + let db = context.db(); + + let Some(builder) = context.report_lint(&INVALID_TOTAL_ORDERING, decorator) else { + return; + }; + + let mut diagnostic = builder.into_diagnostic( + "Class decorated with `@total_ordering` must define at least one ordering method", + ); + diagnostic.set_primary_message(format_args!( + "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", + class.name(db) + )); + diagnostic.info("The decorator will raise `ValueError` at runtime"); +} + /// This function receives an unresolved `from foo import bar` import, /// where `foo` can be resolved to a module but that module does not /// have a `bar` member or submodule. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7a057380e9..e018784643 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -77,7 +77,7 @@ use crate::types::diagnostic::{ report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, - report_invalid_or_unsupported_base, report_invalid_return_type, + report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering, report_invalid_type_checking_constant, report_invalid_type_param_order, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable, @@ -852,7 +852,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (5) Check that the class's metaclass can be determined without error. + // (5) Check that @total_ordering has a valid ordering method in the MRO + if class.total_ordering(self.db()) { + let has_ordering_method = class + .iter_mro(self.db(), None) + .filter_map(super::super::class_base::ClassBase::into_class) + .filter(|base_class| { + !base_class + .class_literal(self.db()) + .0 + .is_known(self.db(), KnownClass::Object) + }) + .any(|base_class| { + base_class + .class_literal(self.db()) + .0 + .has_own_ordering_method(self.db()) + }); + + if !has_ordering_method { + // Find the @total_ordering decorator to report the diagnostic at its location + if let Some(decorator) = class_node.decorator_list.iter().find(|decorator| { + self.expression_type(&decorator.expression) + .as_function_literal() + .is_some_and(|function| { + function.is_known(self.db(), KnownFunction::TotalOrdering) + }) + }) { + report_invalid_total_ordering(&self.context, class, decorator); + } + } + } + + // (6) Check that the class's metaclass can be determined without error. if let Err(metaclass_error) = class.try_metaclass(self.db()) { match metaclass_error.reason() { MetaclassErrorKind::Cycle => { diff --git a/ty.schema.json b/ty.schema.json index 9b65d7e24e..0edef9201b 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -786,6 +786,16 @@ } ] }, + "invalid-total-ordering": { + "title": "detects `@total_ordering` classes without an ordering method", + "description": "## What it does\nChecks for classes decorated with `@functools.total_ordering` that don't\ndefine any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).\n\n## Why is this bad?\nThe `@total_ordering` decorator requires the class to define at least one\nordering method. If none is defined, Python raises a `ValueError` at runtime.\n\n## Example\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass: # Error: no ordering method defined\n def __eq__(self, other: object) -> bool:\n return True\n```\n\nUse instead:\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass:\n def __eq__(self, other: object) -> bool:\n return True\n\n def __lt__(self, other: \"MyClass\") -> bool:\n return True\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-type-alias-type": { "title": "detects invalid TypeAliasType definitions", "description": "## What it does\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n```python\nfrom typing import TypeAliasType\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\nNewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal\n```",