diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 128b470520b9..b0bfcdbd1d51 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3797,6 +3797,15 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: right_type = try_getting_literal(right_type) self.msg.dangerous_comparison(left_type, right_type, "equality", e) + # For ordering comparisons on tuples, verify that element types + # actually support the comparison. The tuple stubs use a covariant + # TypeVar which can allow the reverse operator to pass even when + # element types don't support the comparison at runtime. + if not w.has_new_errors() and operator in ("<", ">", "<=", ">="): + right_type = self.accept(right) + if not self.chk.can_skip_diagnostics: + self._check_tuple_element_comparison(operator, left_type, right_type, e) + elif operator == "is" or operator == "is not": right_type = self.accept(right) # validate the right operand sub_result = self.bool_type() @@ -3967,6 +3976,48 @@ def dangerous_comparison( return False return not is_overlapping_types(left, right, ignore_promotions=False) + def _check_tuple_element_comparison( + self, operator: str, left_type: Type, right_type: Type, context: Context + ) -> None: + """Check that tuple element types support an ordering comparison. + + Tuple comparisons are element-wise at runtime, but the typeshed stubs + use a covariant TypeVar which can allow comparisons to pass at the type + level even when element types don't support the operator. + """ + left_proper = get_proper_type(left_type) + right_proper = get_proper_type(right_type) + + left_elem = self._get_tuple_item_type(left_proper) + right_elem = self._get_tuple_item_type(right_proper) + + if left_elem is None or right_elem is None: + return + + # Skip check if either element type is Any + left_elem_proper = get_proper_type(left_elem) + right_elem_proper = get_proper_type(right_elem) + if isinstance(left_elem_proper, AnyType) or isinstance(right_elem_proper, AnyType): + return + + method = operators.op_methods[operator] + with self.msg.filter_errors() as w: + self.check_op( + method, + left_elem, + TempNode(right_elem, context=context), + context, + allow_reverse=True, + ) + if w.has_new_errors(): + self.msg.unsupported_operand_types(operator, left_type, right_type, context) + + def _get_tuple_item_type(self, typ: ProperType) -> Type | None: + """Get the element type of a homogeneous tuple type, or None if not applicable.""" + if isinstance(typ, Instance) and typ.type.fullname == "builtins.tuple": + return typ.args[0] if typ.args else None + return None + def check_method_call_by_name( self, method: str, diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index 9653d9d037ce..d52dd7a4a6ac 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -1838,3 +1838,59 @@ from typing_extensions import Concatenate def c(t: tuple[Concatenate[int, ...]]) -> None: # E: Cannot use "[int, VarArg(Any), KwArg(Any)]" for tuple, only for ParamSpec reveal_type(t) # N: Revealed type is "tuple[Any]" [builtins fixtures/tuple.pyi] + +[case testTupleComparisonNonComparableElements] +from typing import Tuple +a: Tuple[object, ...] +b: Tuple[object, ...] +c = a < b # E: Unsupported operand types for < ("tuple[object, ...]" and "tuple[object, ...]") +d = a > b # E: Unsupported operand types for > ("tuple[object, ...]" and "tuple[object, ...]") +e = a <= b # E: Unsupported operand types for <= ("tuple[object, ...]" and "tuple[object, ...]") +f = a >= b # E: Unsupported operand types for >= ("tuple[object, ...]" and "tuple[object, ...]") +[builtins fixtures/ops.pyi] + +[case testTupleComparisonComparableElements] +from typing import Tuple +a: Tuple[int, ...] +b: Tuple[int, ...] +c = a < b +d = a > b +e = a <= b +f = a >= b +[builtins fixtures/ops.pyi] + +[case testTupleComparisonOnlyGT] +from typing import Tuple, Any + +class OnlyGT: + def __gt__(self, other: 'OnlyGT') -> bool: ... + +a: Tuple[OnlyGT, ...] +b: Tuple[object, ...] +c = a < b # E: Unsupported operand types for < ("tuple[OnlyGT, ...]" and "tuple[object, ...]") +[builtins fixtures/ops.pyi] + +[case testTupleComparisonOnlyGTValid] +from typing import Tuple, Any + +class OnlyGT: + def __gt__(self, other: 'OnlyGT') -> bool: ... + +a: Tuple[OnlyGT, ...] +b: Tuple[OnlyGT, ...] +c = a > b +[builtins fixtures/ops.pyi] + +[case testTupleComparisonCovariantReverse] +# Regression test for https://github.com/python/mypy/issues/21042 +from typing import Tuple, Any + +class OnlyGT: + def __gt__(self, other: Any) -> bool: ... + +a: Tuple[OnlyGT, ...] +b: Tuple[object, ...] +# Tuple-level reverse op (tuple[object, ...].__gt__) passes due to covariance, +# but element types don't support < +c = a < b # E: Unsupported operand types for < ("tuple[OnlyGT, ...]" and "tuple[object, ...]") +[builtins fixtures/ops.pyi]