Skip to content

Commit c33da74

Browse files
David-Wobrockilevkivskyi
authored andcommitted
Better error message when __eq__ has unexpected signature (#6106)
This PR will add a message and an example when the arguments of the dunder method `__eq__` do not match the expected signature. With this PR, the added message looks like this: ``` It is recommended for "__eq__" to work with arbitrary objects, for example: def __eq__(self, other: object) -> bool: if not isinstance(other, {class_name}): return NotImplemented return <logic to compare two {class_name} instances> ``` Fixes #2055
1 parent 1d40527 commit c33da74

File tree

4 files changed

+46
-11
lines changed

4 files changed

+46
-11
lines changed

mypy/checker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,9 @@ def check_override(self, override: FunctionLike, original: FunctionLike,
14751475
# are erased, then it is definitely an incompatibility.
14761476

14771477
override_ids = override.type_var_ids()
1478+
type_name = None
1479+
if isinstance(override.definition, FuncDef):
1480+
type_name = override.definition.info.name()
14781481

14791482
def erase_override(t: Type) -> Type:
14801483
return erase_typevars(t, ids_to_erase=override_ids)
@@ -1483,7 +1486,7 @@ def erase_override(t: Type) -> Type:
14831486
if not is_subtype(original.arg_types[i],
14841487
erase_override(override.arg_types[i])):
14851488
self.msg.argument_incompatible_with_supertype(
1486-
i + 1, name, name_in_super, supertype, node)
1489+
i + 1, name, type_name, name_in_super, supertype, node)
14871490
emitted_msg = True
14881491

14891492
if not is_subtype(erase_override(override.ret_type),

mypy/errors.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,10 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
405405
str, str]]:
406406
"""Translate the messages into a sequence of tuples.
407407
408-
Each tuple is of form (path, line, col, message. The rendered
409-
sequence includes information about error contexts. The path
410-
item may be None. If the line item is negative, the line
411-
number is not defined for the tuple.
408+
Each tuple is of form (path, line, col, severity, message).
409+
The rendered sequence includes information about error contexts.
410+
The path item may be None. If the line item is negative, the
411+
line number is not defined for the tuple.
412412
"""
413413
result = [] # type: List[Tuple[Optional[str], int, int, str, str]]
414414
# (path, line, column, severity, message)

mypy/messages.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections import OrderedDict
1414
import re
1515
import difflib
16+
from textwrap import dedent
1617

1718
from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Set, Optional, Union
1819

@@ -187,7 +188,7 @@ def report(self, msg: str, context: Optional[Context], severity: str,
187188
if self.disable_count <= 0:
188189
self.errors.report(context.get_line() if context else -1,
189190
context.get_column() if context else -1,
190-
msg.strip(), severity=severity, file=file, offset=offset,
191+
msg, severity=severity, file=file, offset=offset,
191192
origin_line=origin.get_line() if origin else None)
192193

193194
def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None,
@@ -198,7 +199,15 @@ def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None,
198199
def note(self, msg: str, context: Context, file: Optional[str] = None,
199200
origin: Optional[Context] = None, offset: int = 0) -> None:
200201
"""Report a note (unless disabled)."""
201-
self.report(msg, context, 'note', file=file, origin=origin, offset=offset)
202+
self.report(msg, context, 'note', file=file, origin=origin,
203+
offset=offset)
204+
205+
def note_multiline(self, messages: str, context: Context, file: Optional[str] = None,
206+
origin: Optional[Context] = None, offset: int = 0) -> None:
207+
"""Report as many notes as lines in the message (unless disabled)."""
208+
for msg in messages.splitlines():
209+
self.report(msg, context, 'note', file=file, origin=origin,
210+
offset=offset)
202211

203212
def warn(self, msg: str, context: Context, file: Optional[str] = None,
204213
origin: Optional[Context] = None) -> None:
@@ -854,12 +863,25 @@ def signature_incompatible_with_supertype(
854863
name, target), context)
855864

856865
def argument_incompatible_with_supertype(
857-
self, arg_num: int, name: str, name_in_supertype: str,
858-
supertype: str, context: Context) -> None:
866+
self, arg_num: int, name: str, type_name: Optional[str],
867+
name_in_supertype: str, supertype: str, context: Context) -> None:
859868
target = self.override_target(name, name_in_supertype, supertype)
860869
self.fail('Argument {} of "{}" incompatible with {}'
861870
.format(arg_num, name, target), context)
862871

872+
if name == "__eq__" and type_name:
873+
multiline_msg = self.comparison_method_example_msg(class_name=type_name)
874+
self.note_multiline(multiline_msg, context)
875+
876+
def comparison_method_example_msg(self, class_name: str) -> str:
877+
return dedent('''\
878+
It is recommended for "__eq__" to work with arbitrary objects, for example:
879+
def __eq__(self, other: object) -> bool:
880+
if not isinstance(other, {class_name}):
881+
return NotImplemented
882+
return <logic to compare two {class_name} instances>
883+
'''.format(class_name=class_name))
884+
863885
def return_type_incompatible_with_supertype(
864886
self, name: str, name_in_supertype: str, supertype: str,
865887
context: Context) -> None:
@@ -1110,8 +1132,6 @@ def reveal_locals(self, type_map: Dict[str, Optional[Type]], context: Context) -
11101132
# use an ordered dictionary sorted by variable name
11111133
sorted_locals = OrderedDict(sorted(type_map.items(), key=lambda t: t[0]))
11121134
self.fail("Revealed local types are:", context)
1113-
# Note that self.fail does a strip() on the message, so we cannot prepend with spaces
1114-
# for indentation
11151135
for line in ['{}: {}'.format(k, v) for k, v in sorted_locals.items()]:
11161136
self.fail(line, context)
11171137

test-data/unit/check-classes.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,18 @@ class B(A):
282282
main:7: error: Argument 1 of "f" incompatible with supertype "A"
283283
main:9: error: Return type of "h" incompatible with supertype "A"
284284

285+
[case testEqMethodsOverridingWithNonObjects]
286+
class A:
287+
def __eq__(self, other: A) -> bool: pass # Fail
288+
[builtins fixtures/attr.pyi]
289+
[out]
290+
main:2: error: Argument 1 of "__eq__" incompatible with supertype "object"
291+
main:2: note: It is recommended for "__eq__" to work with arbitrary objects, for example:
292+
main:2: note: def __eq__(self, other: object) -> bool:
293+
main:2: note: if not isinstance(other, A):
294+
main:2: note: return NotImplemented
295+
main:2: note: return <logic to compare two A instances>
296+
285297
[case testMethodOverridingWithIncompatibleArgumentCount]
286298
import typing
287299
class A:

0 commit comments

Comments
 (0)