-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Better error message when __eq__ has unexpected signature #6106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The result tuple type was missing a field
and __ne__ dunder methods signatures. When the 'other' argument is not an 'object' type, we show a better error message with an example code. * And add a test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the PR! Here I have some comments and suggestions.
mypy/messages.py
Outdated
origin_line=origin.get_line() if origin else None) | ||
|
||
def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None, | ||
origin: Optional[Context] = None) -> None: | ||
origin: Optional[Context] = None, strip_msg: bool = True) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add the new strip_msg
argument only where it is actually used, i.e. in notes. It is unlikely that we will have multiline errors or warnings.
mypy/messages.py
Outdated
|
||
def warn(self, msg: str, context: Context, file: Optional[str] = None, | ||
origin: Optional[Context] = None) -> None: | ||
origin: Optional[Context] = None, strip_msg: bool = True) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
mypy/messages.py
Outdated
|
||
def note_multiline(self, messages: str, context: Context, file: Optional[str] = None, | ||
origin: Optional[Context] = None, offset: int = 0, | ||
strip_msg: bool = True) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think strip_msg
is not needed for note_multiline
. It is quite clear that multiline notes will be often indented.
self.errors.report(context.get_line() if context else -1, | ||
context.get_column() if context else -1, | ||
msg.strip(), severity=severity, file=file, offset=offset, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some thinking I am not sure why do we actually need to strip the message here. How many tests fail if you remove the .strip()
here? Maybe we can instead update our test framework?
mypy/messages.py
Outdated
@@ -860,6 +871,21 @@ def argument_incompatible_with_supertype( | |||
self.fail('Argument {} of "{}" incompatible with {}' | |||
.format(arg_num, name, target), context) | |||
|
|||
if name in ("__eq__", "__ne__"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this note should be applied only for __eq__
.
mypy/messages.py
Outdated
return '''It is recommended for "{method_name}" to work with arbitrary objects. | ||
The snippet below shows an example of how you can implement "{method_name}": | ||
|
||
class Foo(...): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the actual class name instead here?
mypy/messages.py
Outdated
class Foo(...): | ||
... | ||
def {method_name}(self, other: object) -> bool: | ||
if not isinstance(other, Foo): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...and here
test-data/unit/check-classes.test
Outdated
main:2: error: Argument 1 of "__eq__" incompatible with supertype "object" | ||
main:2: note: It is recommended for "__eq__" to work with arbitrary objects. | ||
main:2: note: The snippet below shows an example of how you can implement "__eq__": | ||
main:2: note: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "preamble" is too wordy. I would use shorter It is recommended for "__eq__" to work with arbitrary objects, for example:
and skip the empty line.
test-data/unit/check-classes.test
Outdated
main:2: note: The snippet below shows an example of how you can implement "__eq__": | ||
main:2: note: | ||
main:2: note: class Foo(...): | ||
main:2: note: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually skip these two lines (however I would keep the indentation of the next lines).
mypy/messages.py
Outdated
def {method_name}(self, other: object) -> bool: | ||
if not isinstance(other, Foo): | ||
raise NotImplementedError | ||
return <logic to compare two Foo instances>'''.format(method_name=method_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here I would also use the actual class name instead of Foo
.
(I also edited the PR description to add the "magic" text for GitHub.) |
* Delete outdated comment
* Use textwrap.dedent to have a nicer multiline string * Only show the note for __eq__ * Use the actual class name in the shown code snippet * Adapt the test
7ee376e
to
a630c67
Compare
Hey @ilevkivskyi, thanks for the comments. The PR is open for review again and I took your changes into account in the last 2 commits:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for updates! This is now almost ready, I just have three small comments.
mypy/messages.py
Outdated
offset=offset) | ||
|
||
def note_multiline(self, messages: str, context: Context, file: Optional[str] = None, | ||
origin: Optional[Context] = None, offset: int = 0) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the same style as everywhere else: indent for argument coincides with the start of first argument.
test-data/unit/check-classes.test
Outdated
main:2: note: It is recommended for "__eq__" to work with arbitrary objects, for example: | ||
main:2: note: def __eq__(self, other: object) -> bool: | ||
main:2: note: if not isinstance(other, A): | ||
main:2: note: raise NotImplementedError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think return NotImplemented
is more idiomatic, but I don't think this should delay this PR if there are other opinions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, NotImplemented
is right here. It signals to the runtime to try the other object's __eq__
.
mypy/messages.py
Outdated
@@ -860,6 +869,20 @@ def argument_incompatible_with_supertype( | |||
self.fail('Argument {} of "{}" incompatible with {}' | |||
.format(arg_num, name, target), context) | |||
|
|||
if name == "__eq__": | |||
assert isinstance(context, FuncDef) | |||
multiline_msg = self.comparison_method_example_msg(class_name=context.info.name()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting anything except line/column from context
is a bad idea. First, this kind of defeats the purpose of the type annotation. Second, what if this is an OverloadedFuncDef
, or a Decorator
?
I think it is better to add an optional argument current_type_name
to argument_incompatible_with_supertype
, and if name == '__eq__': assert current_type_name is not None
etc. (Or maybe even make it a required argument if there are only couple of call sites to update).
Instead of inspecting the context, we directly pass the type name to the messages methods as argument.
d247fde
to
9854898
Compare
Hi @ilevkivskyi, thanks again for the feedback, and thank you @JelleZijlstra. Again, the last 3 commits contain the suggested changes. Cheers and happy new year! |
mypy/checker.py
Outdated
if isinstance(override.definition, FuncDef): | ||
type_name = override.definition.info.name() | ||
else: | ||
type_name = "Foo" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a better alternative is to not show the note at all if __eq__
is not a function definition. So you can just set type_name
to None
here, and add a check in argument_incompatible_with_supertype()
that will not show the note if the name is None
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, looks ready now!
Type added in a way recommended at python/mypy#6106
Type added in a way recommended at python/mypy#6106
Hey there,
This PR tackles an old issue #2055 opened in 2016 - to get started in the mypy source code.
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:The 3 commits can and should be reviewed individually since they represent independent bulks of work:
Looking forward for some reviews
Cheers,
David
Fixes #2055