-
-
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
ilevkivskyi
left a comment
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
|
|
||
| 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.
| msg = msg.strip() if strip_msg else msg | ||
| 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
| 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:
|
ilevkivskyi
left a comment
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
|
|
||
| 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.
ilevkivskyi
left a comment
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