-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Type check arguments for "super" #3919
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
This is not quite as strict as we could be, but this is still better than what we had before. A full implementation will be somewhat complicated. Remaining issues: * Assume that the first argument refers to the enclosing class, even though that's not generally the case. * Assume that a type object as the second argument is always fine. * Don't validate some kinds of type object types as the first argument. * Single-argument variant is not supported. Fixes #526 (modulo the open issues). Also, I no longer think that defining messages as constants in `mypy.messages` is generally a good idea so I'm not following that pattern here. I'll officially update the convention in a separate PR.
This is ready for review but this is not ready to be merged yet, as this requires some changes to internal Dropbox code. I'll create follow-up issues for any remaining open issues after this has been merged. |
Appveyor failure is a flake (see #3895) |
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! This is now the oldest high priority issue, I am glad it will be fixed now.
elif len(e.call.args) > 2: | ||
self.chk.fail('Too many arguments for "super"', e) | ||
elif self.chk.options.python_version[0] == 2 and len(e.call.args) == 0: | ||
self.chk.fail('Too few arguments for "super"', e) |
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 move all the logic until this point to semanal.py
, but it is optional.
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'm keeping the logic here because we want to execute the final elif
block only if all the previous checks are false, and having all the checks here makes it explicit. Also, we check the argument counts of other calls during type checking (though super()
is special so we could do it during semantic analysis as well).
mypy/checkexpr.py
Outdated
elif isinstance(type_obj_type, AnyType): | ||
return | ||
else: | ||
self.chk.fail('First argument for "super" must be a type object', e) |
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 Got: "<actual type>"
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.
Updated, but I still don't display the class name, since it looked confusing (due to type of instance C
looking like a reference C
to type object).
mypy/checkexpr.py
Outdated
instance_type = instance_type.upper_bound | ||
if not isinstance(instance_type, (Instance, TupleType)): | ||
# Too tricky, give up. | ||
self.chk.fail('Unsupported argument 2 for "super"', e) |
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 is a bit inconsistent with the first argument logic above, where we give up by a simple return
without any error message, why it is so?
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 now generate error above as well.
mypy/checkexpr.py
Outdated
# TODO: Check whether this is a valid type object here. | ||
pass | ||
elif not isinstance(instance_type, AnyType): | ||
self.chk.fail('Unsupported argument 2 for "super"', e) |
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.
My question above about consistency also applies here. Also in this case the error message is repeated, so maybe it makes sense to define a constant for it?
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.
Updated to use a constant for the error message.
elif not isinstance(instance_type, AnyType): | ||
self.chk.fail('Unsupported argument 2 for "super"', e) | ||
|
||
def analyze_super(self, e: SuperExpr, is_lvalue: bool) -> Type: |
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 remember looking at the function analyze_super
and something worried me. Namely, look at the error:
super() requires at least one positional argument
several lines below (can't comment there), it is misleading since it can appear on Python 3 as well but we support super()
without arguments. Also the logic there seems a bit wrong.
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.
The message refers to the enclosing function not having any positional arguments. Updated the wording. If it's still incorrect, we can create a new issue about it, since this looks like a pretty marginal scenario. The logic is kind of convoluted though -- most of the for loop body is executed at most once, which is not immediately obvious.
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.
You already have a list of things still left to fix in the PR description, if you feel this deserves a separate issue, then OK, otherwise you can just combine this with first item in your list.
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.
It's still unclear to me what the issue is (other than the code being hard to understand). Can you create a new issue (or add a comment to an existing relevant issue) about this?
mypy/treetransform.py
Outdated
|
||
def visit_super_expr(self, node: SuperExpr) -> SuperExpr: | ||
new = SuperExpr(node.name) | ||
new = SuperExpr(node.name, cast(CallExpr, self.expr(node.call))) |
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.
Why a cast needed here? Is it possible to replace it with an assert?
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.
Replaced with an assert.
super(C, self).xyz | ||
|
||
[case testSuperWithTypeType] | ||
from typing import Type |
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.
You are not using this import, have you forgotten to test something here?
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, added another thing to test.
test-data/unit/check-super.test
Outdated
return self | ||
|
||
[case testSuperWithTypeVarValues] | ||
from typing import TypeVar, Generic |
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.
You imported Generic
but didn't use it. Did you want to add another test also here? I would actually add some tests with Generic
.
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.
Added a test that uses Generic
.
test-data/unit/check-super.test
Outdated
def f(self) -> None: pass | ||
|
||
class C(B): | ||
def f(self) -> None: pass |
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 make return types different here and in superclass (e.g. float
and int
), so that we could be sure in reveal_type
that the correct one is picked up.
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.
Updated.
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.
Just one more question. You can merge this when the internal changes you mentioned are ready.
mypy/checkexpr.py
Outdated
if not isinstance(item, Instance): | ||
# A complicated type object type. Too tricky, give up. | ||
# TODO: Do something more clever here. | ||
self.chk.fail(messages.UNSUPPORTED_ARGUMENT_2_FOR_SUPER, e) |
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.
Should it be actually argument 1 here?
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! Added a test case and fixed the error message.
This is not quite as strict as we could be, but this is still better
than what we had before. A full implementation will be somewhat
complicated.
Remaining issues:
Assume that the first argument refers to the enclosing class, even
though that's not generally the case.
Assume that a type object as the second argument is always fine.
Don't validate some kinds of type object types as the first argument.
Single-argument variant is not supported.
Fixes #526 (modulo the open issues).
Also, I no longer think that defining messages as constants in
mypy.messages
is generally a good idea so I'm not followingthat pattern here. I'll officially update the convention in a
separate PR.