-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Fix checking of self when accessing a non-method callable attribute (take 2) #4016
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
1. Reverse subtyping check for self argument 2. For classmethod, fallback on the argument instead of on the parameter 3. Mimic dispatch better: meet original_type with static class 4. Better error message
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 the PR! Note that this is not a full review, just some quick notes.
Can you update the PR message to describe the problem that this PR fixes, with an example? Currently I need to look up the issue to understand what is going on. Another nit is that the title of the PR doesn't communicate the purpose of this PR very well, but maybe it's hard to summarize it better.
I checked that this doesn't cause any issues with internal Dropbox codebases.
mypy/checkmember.py
Outdated
context: Context, msg: MessageBuilder) -> None: | ||
def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: bool, | ||
context: Context, name: str, msg: MessageBuilder) -> None: | ||
"""Check that the the most precise type of the self argument is compatible |
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.
Style nit: in a multi-line docstring there should be an empty line after the first line.
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'll fix it on the next commit or before merge.
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 docstring could also be clarified a bit. The meaning of "the most precise type of the self argument" wasn't clear initially. Maybe something about generic self, and using the actual type of the self (based on the receiver type) rather than the declared type in the function signature? Would also be good to describe original_type
here -- it would even make sense to rename it. What about actual_self_type
? It's always the type of self, and it's the actual instead of the formal/declared type. If you rename it, it would also make sense to rename it elsewhere.
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.
There are 4 potentially distinct types in play here:
- The declared type of the first parameter
self
(potentially generic) - The inferred/declared type of
x
inx.f
, ignoring the function signature - The same type as (2) but after narrowing it using the current class
- The type of the current class
(4) is almost irrelevant at this point, but it is used to construct (3), restricts the valid types for (2), and adds to the potential confusion.
So both (2) and (3) are "actual self type". Maybe I should elaborate on the docstring/comment, but it will become lengthy.
Updated the PR description and the title. I hope it's an improvement. |
Sorry, this is too late to make it into the 0.530 release. It's a minor issue and it touches subtle parts of the codebase. Let's postpone 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.
Thanks for fixing this -- it now looks cleaner (plus it also works better!). This concludes my review.
Some of the self type related code is still kind of hard to understand -- I added some ideas below for how to clarify things. If some of the suggested changes would require many additional changes (such as renaming original_type
to something else) it's probably better to do that as a separate PR.
mypy/checkmember.py
Outdated
@@ -314,7 +316,9 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont | |||
# methods: the former to the instance, the latter to the | |||
# class. | |||
functype = t | |||
check_method_type(functype, itype, var.is_classmethod, node, msg) | |||
# Use meet to simulate dispatch - e.g. reduce Union[A, B] to A on dispatch to A |
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 comment is unclear. Can you add a comment with an example where this makes a difference?
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 added a comment and a test. This kind of dispatch with not-exactly-the-right-type occur in reverse operators.
I believe the same thing should also be done with plain functions, but it's not checked at all now. i.e. there should be a call to check_self_arg
before bind_self
here. For example:
class A:
def f(self: B) -> None: pass # type: ignore - because reasons. probably useful for metaclasses
class B(A): pass
A().f() # should be an error
but changing plain functions breaks some typeshed code (specifically, AbstractSet operators and the version_info
tests break).
mypy/checkmember.py
Outdated
selfarg = selfarg.fallback | ||
if not subtypes.is_subtype(selfarg, itype): | ||
msg.invalid_method_type(item, context) | ||
msg.fail('Attribute function with type %s does not accept self argument' |
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.
Add a method to Messages
instead of building the message here.
test-data/unit/check-selftype.test
Outdated
|
||
class B(A): | ||
pass | ||
|
||
reveal_type(X().__members__) # E: Revealed type is '__main__.X*' |
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.
Would it make sense to add a test case similar to this, but so that X
would a subtype of Tuple
?
mypy/checkmember.py
Outdated
context: Context, msg: MessageBuilder) -> None: | ||
def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: bool, | ||
context: Context, name: str, msg: MessageBuilder) -> None: | ||
"""Check that the the most precise type of the self argument is compatible |
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 docstring could also be clarified a bit. The meaning of "the most precise type of the self argument" wasn't clear initially. Maybe something about generic self, and using the actual type of the self (based on the receiver type) rather than the declared type in the function signature? Would also be good to describe original_type
here -- it would even make sense to rename it. What about actual_self_type
? It's always the type of self, and it's the actual instead of the formal/declared type. If you rename it, it would also make sense to rename it elsewhere.
@JukkaL I will only be available for a couple of hours from now, so I won't be able to respond to further comments in time, unless ~30 hours from now will be good enough. As I understand it, the behavior is OK, and I hope it will be possible to resolve further issues in docstrings and comments even after the branch is cut. |
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 a few minor things. If you can't make the updates before I cut the release branch this Friday, I can merge this as such before cutting the branch.
test-data/unit/check-selftype.test
Outdated
|
||
[case testSelfTypeProperSupertypeAttribute] | ||
from typing import Callable, TypeVar | ||
T = TypeVar('T', bound=object) |
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.
bound=object
is redundant -- this is equivalent to T = TypeVar('T')
. It would be nice to have a test case with a non-object
bound.
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 know it's redundant, I just wanted to be clear about the intention of the test. But yes, I should add a non-object bound
test-data/unit/check-selftype.test
Outdated
reveal_type(X.g) # E: Revealed type is 'builtins.int' | ||
reveal_type(X.gt) # E: Revealed type is 'def () -> __main__.X' | ||
reveal_type(X.f()) # E: Revealed type is 'builtins.int' | ||
reveal_type(X.ft()) # E: Revealed type is 'def () -> __main__.X' |
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 you add a __init__
to X
that takes an argument so that it's obvious that the signature is derived from the constructor?
a: A[int] | ||
b: B[str] | ||
reveal_type(a.g) # E: Revealed type is 'builtins.int' | ||
--reveal_type(a.gt) # E: Revealed type is 'builtins.int' |
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 are these commented out?
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 instantiation of selftype fails in the commented tests. I think it is not a problem in this PR, and fixing these requires changes in bind_self
.
I just forced an update that removed two of the comments; I hope it doesn't cause a mess.
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.
Sorry - I didn't force it; I will add as a new commit in a moment.
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.
To elaborate, bind_self
does not unify K[T]
and A[T]
even if it can. It just does not check for this case, and simply trims the first argument. It only performs such unification is for TypeType.
385a5f6
to
a989775
Compare
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 good.
Fix #3223. Minimal examples:
The problem is that for non-methods the check performed on access to
self
is whetherobject <: X
instead of the other way around.original_type
with static class. This handles the case were instead ofX()
above there was something of typeUnion[X, Y]
.Take 2 of #3227, but without the reverse-operator part