Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3706,6 +3706,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()
Expand Down Expand Up @@ -3876,6 +3885,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,
Expand Down
56 changes: 56 additions & 0 deletions test-data/unit/check-tuples.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading