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
6 changes: 5 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4368,7 +4368,11 @@ def check_op_reversible(
results = []
for name, method, obj, arg in variants:
with self.msg.filter_errors(save_filtered_errors=True) as local_errors:
result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context)
# Track the operator as written, so that errors from a reflected
# call (e.g. `x >= 0` checked as `int.__le__(x)`) report the
# source operator instead of the reflected one. See #22316.
with self.msg.operator_context(op_name):
result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context)
if local_errors.has_new_errors():
errors.append(local_errors.filtered_errors())
results.append(result)
Expand Down
46 changes: 45 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
get_func_def,
reverse_builtin_aliases,
)
from mypy.operators import op_methods, op_methods_to_symbols
from mypy.operators import op_methods, op_methods_to_symbols, reverse_op_methods
from mypy.options import Options
from mypy.subtypes import (
IS_CLASS_OR_STATIC,
Expand Down Expand Up @@ -180,11 +180,17 @@ class MessageBuilder:
# Hack to deduplicate error messages from union types
_disable_type_names: list[bool]

# Stack of operator method names for binary operator checks currently
# in progress (see check_op_reversible). The top of the stack is the
# operator as written in the source code, e.g. "__ge__" for `x >= 0`.
_operator_context: list[str]

def __init__(self, errors: Errors, modules: dict[str, MypyFile]) -> None:
self.errors = errors
self.options = errors.options
self.modules = modules
self._disable_type_names = []
self._operator_context = []

#
# Helpers
Expand Down Expand Up @@ -219,6 +225,22 @@ def disable_type_names(self) -> Iterator[None]:
finally:
self._disable_type_names.pop()

@contextmanager
def operator_context(self, op_name: str) -> Iterator[None]:
"""Track the operator currently being checked, for error reporting.

When a binary operation is checked via the reflected method on the
other operand (e.g. `x >= 0` checked as `int.__le__(x)` because the
left operand has no `__ge__`), errors about the call would otherwise
name the reflected operator. This context lets incompatible_argument()
report the operator as written in the source code instead. See #22316.
"""
self._operator_context.append(op_name)
try:
yield
finally:
self._operator_context.pop()

def are_type_names_disabled(self) -> bool:
return len(self._disable_type_names) > 0 and self._disable_type_names[-1]

Expand Down Expand Up @@ -643,6 +665,28 @@ def incompatible_argument(
)
return self.fail(error_msg.value, context, code=error_msg.code)
elif name.startswith('"__'):
# If this comparison is being checked via the reflected method
# on the other operand (e.g. `x >= 0` failing as `int.__le__(x)`
# because the left operand has no `__ge__`), report the operator
# as written in the source code, with operands in source order,
# instead of the reflected operator. See #22316.
if self._operator_context:
source_op = self._operator_context[-1]
if source_op in ("__lt__", "__le__", "__gt__", "__ge__"):
for method in (source_op, reverse_op_methods[source_op]):
if name.startswith(f'"{method}" of'):
op = op_methods_to_symbols[source_op]
if method == source_op:
return self.unsupported_operand_types(
op, base, arg_type, context, code=codes.OPERATOR
)
else:
# Reflected call: the argument is the source
# left operand and the base is the source
# right operand.
return self.unsupported_operand_types(
op, arg_type, base, context, code=codes.OPERATOR
)
for method, op in op_methods_to_symbols.items():
for variant in method, "__r" + method[2:]:
# FIX: do not rely on textual formatting
Expand Down
22 changes: 21 additions & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,27 @@ if int():
s = A() < B()
if int():
n = A() < B() # E: Incompatible types in assignment (expression has type "str", variable has type "int")
s = object() < B() # E: Unsupported operand types for > ("B" and "object")
s = object() < B() # E: Unsupported operand types for < ("object" and "B")

[case testComparisonOperatorMessageWithNone]
# Regression test for #22316: the error for a comparison must name the
# operator as written, with operands in source order, instead of the
# reflected operator used to check it.
def f(x: int | None) -> bool:
return x >= 0 # E: Unsupported operand types for >= ("None" and "int") \
# N: Left operand is of type "int | None"
def g(x: int | None) -> bool:
return x <= 0 # E: Unsupported operand types for <= ("None" and "int") \
# N: Left operand is of type "int | None"
def h(x: int | None) -> bool:
return x > 0 # E: Unsupported operand types for > ("None" and "int") \
# N: Left operand is of type "int | None"
def k(x: int | None) -> bool:
return x < 0 # E: Unsupported operand types for < ("None" and "int") \
# N: Left operand is of type "int | None"
def ok(x: int, s: str) -> bool:
return x >= s # E: Unsupported operand types for >= ("int" and "str")
[builtins fixtures/ops.pyi]

[case testReversibleComparisonWithExtraArgument]
class C:
Expand Down
24 changes: 12 additions & 12 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -3097,10 +3097,10 @@ A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int")
A(1) == 1
A(1) != 1

1 < A(1) # E: Unsupported operand types for > ("A" and "int")
1 <= A(1) # E: Unsupported operand types for >= ("A" and "int")
1 > A(1) # E: Unsupported operand types for < ("A" and "int")
1 >= A(1) # E: Unsupported operand types for <= ("A" and "int")
1 < A(1) # E: Unsupported operand types for < ("int" and "A")
1 <= A(1) # E: Unsupported operand types for <= ("int" and "A")
1 > A(1) # E: Unsupported operand types for > ("int" and "A")
1 >= A(1) # E: Unsupported operand types for >= ("int" and "A")
1 == A(1)
1 != A(1)

Expand All @@ -3123,10 +3123,10 @@ main:15: error: Unsupported operand types for < ("A" and "int")
main:16: error: Unsupported operand types for <= ("A" and "int")
main:17: error: Unsupported operand types for > ("A" and "int")
main:18: error: Unsupported operand types for >= ("A" and "int")
main:22: error: Unsupported operand types for > ("A" and "int")
main:23: error: Unsupported operand types for >= ("A" and "int")
main:24: error: Unsupported operand types for < ("A" and "int")
main:25: error: Unsupported operand types for <= ("A" and "int")
main:22: error: Unsupported operand types for < ("int" and "A")
main:23: error: Unsupported operand types for <= ("int" and "A")
main:24: error: Unsupported operand types for > ("int" and "A")
main:25: error: Unsupported operand types for >= ("int" and "A")

[case testAttrsIncrementalSubclassModified]
from b import B
Expand Down Expand Up @@ -4010,10 +4010,10 @@ tmp/b.py:18: error: Unsupported operand types for < ("A" and "int")
tmp/b.py:19: error: Unsupported operand types for <= ("A" and "int")
tmp/b.py:20: error: Unsupported operand types for > ("A" and "int")
tmp/b.py:21: error: Unsupported operand types for >= ("A" and "int")
tmp/b.py:25: error: Unsupported operand types for > ("A" and "int")
tmp/b.py:26: error: Unsupported operand types for >= ("A" and "int")
tmp/b.py:27: error: Unsupported operand types for < ("A" and "int")
tmp/b.py:28: error: Unsupported operand types for <= ("A" and "int")
tmp/b.py:25: error: Unsupported operand types for < ("int" and "A")
tmp/b.py:26: error: Unsupported operand types for <= ("int" and "A")
tmp/b.py:27: error: Unsupported operand types for > ("int" and "A")
tmp/b.py:28: error: Unsupported operand types for >= ("int" and "A")

[case testIncrementalDataclassesSubclassModified]
from b import B
Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/check-plugin-attrs.test
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int")
A(1) == 1
A(1) != 1

1 < A(1) # E: Unsupported operand types for > ("A" and "int")
1 <= A(1) # E: Unsupported operand types for >= ("A" and "int")
1 > A(1) # E: Unsupported operand types for < ("A" and "int")
1 >= A(1) # E: Unsupported operand types for <= ("A" and "int")
1 < A(1) # E: Unsupported operand types for < ("int" and "A")
1 <= A(1) # E: Unsupported operand types for <= ("int" and "A")
1 > A(1) # E: Unsupported operand types for > ("int" and "A")
1 >= A(1) # E: Unsupported operand types for >= ("int" and "A")
1 == A(1)
1 != A(1)
[builtins fixtures/plugin_attrs.pyi]
Expand Down
Loading