-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Sanity checks for declared selftype #2381
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
return cls() | ||
|
||
class D: | ||
def foo(self: str) -> str: # E: The type of self 'builtins.str' is not a supertype its class '__main__.D' |
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.
Outside a stub file, we could allow a function like this in a function body, assuming that the first argument is not named self
(see discussion at #2374 and #16). However, the function shouldn't be usable as an instance method. I've heard about production code that relies on this. It's a questionable idiom, as a missing self
is usually an error, and if we allow this idiom it can result in false negatives.
@gvanrossum What do you think?
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 like to try this out on our two big internal repos. If this finds no issues, I think we should not do this. (If there are complaints we can relent, but if it's rare people can just use # type: ignore
.)
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.
FYI I ran this over our "S" repo and it found 7 legitimate issues, no cases where a function was meant to be called from inside the class, and one mystery that I still have to debug (it was a long-form Python 2.7 signature annotation on a class method, but it correctly left out the 'cls' argument's type -- there might be something wrong due to a lambda used as a default value in one case.
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.
In the "C" repo, I found this:
class C:
def __new__(cls: 'Type[C]') -> 'C':
pass
Would be good to accept such usage.
In that repo this PR also found a few bad annotations but no occurrences of the mythical questionable idiom. :-)
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.
Here's a repro for the first mystery; it's indeed related to a lambda:
from typing import Callable
class C:
@classmethod
def foo(cls,
arg: Callable[[int], str] = lambda a: ''
):
pass
This gives an error on the 'def' line:
__tmp__.py: note: In member "foo" of class "C":
__tmp__.py:4: error: The type of self 'builtins.int' is not a supertype its class '__tmp__.C'
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 actually the mythical questionable idiom! In some sense at least :)
ref_type = mypy.types.TypeType(ref_type) | ||
erased = erase_to_bound(arg_type) | ||
if not is_subtype_ignoring_tvars(ref_type, erased): | ||
self.fail("The type of self '{}' is not a supertype its class '{}'" |
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.
Missing 'of'.
|
Thanks! This found 11 bad annotations in our internal code.
|
python/typeshed#638 is now fixed. I am syncing typeshed in master, once that lands (in about 20 minutes I hope) can you rebase this and see if you can get the tests to pass? |
Sure. But note that the problem with |
I will try to solve the (One of these days I will learn how to rebase) |
Unrelated to what? I see you added tests for it. If those tests were just to help you understand my bug report, that's fine, and you can remove them again. |
The current problem with
Even though class B inherits from A. |
Dang. Wasted a lot of bandwidth because you left out "typeshed". :-(
|
Sorry. |
There was an actual type mismatch in Any suggestions for a better error message? Perhaps there should be a different one for classmethods. |
Thanks, I'll see if I can dive into this once I'm done with another big code review. |
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.
Gotta run, I didn't look at much of the code yet, but here's a start.
if class_type: | ||
self.class_context.append(class_type) | ||
|
||
# Shouldn't we freeze the entire scope? |
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.
That comment should go in the next function.
@@ -508,7 +502,6 @@ def check_func_item(self, defn: FuncItem, | |||
if isinstance(defn, FuncDef): | |||
fdef = defn | |||
|
|||
self.function_stack.append(defn) |
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.
Curious why you don't need scope.push_function() 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.
Because the signature is checked in the scope of the class. I have moved the push to line 645, just before checking the body. Is it 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.
Oh, that sounds fine.
return None | ||
|
||
@contextmanager | ||
def enter_function(self, item: FuncItem) -> Iterator[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.
Shouldn't this use push/pop_function() to make it clear it's doing the right thing? Ditto enter_class().
yield | ||
self.stack.pop() | ||
|
||
def is_method(self) -> bool: |
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 give this a clearer name, e.g. is_within_method()
? And isn't it really checking whether we're inside a class?
self.stack.append(item) | ||
|
||
def pop_function(self) -> None: | ||
assert isinstance(self.stack[-1], 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.
This seems oddly mismatched with the argument type of push_function(). And since the assert never failed in the tests I wonder if this code is exercised at all?
|
||
def pop_function(self) -> None: | ||
assert isinstance(self.stack[-1], Type) | ||
self.stack.pop() |
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.
Maybe assert that the stack is not now empty? (Then also in the other pop methods.)
self.class_context.append(class_type) | ||
|
||
# Shouldn't we freeze the entire scope? | ||
if active_class: |
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.
If you rewrite this as
if active_class:
with self.scope.push_class(active_class):
self.accept(node)
else:
self.accept(node)
you won't need the Scope.push/pop methods at all!
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 just did this :)
arg_type) | ||
# FIX: if i == 0 and this is not a method then same as above | ||
ref_type = self.scope.active_class() | ||
if (isinstance(defn, FuncDef) and ref_type is not None and i == 0 |
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 see the FuncDef check is to fix the lambda case I reported. This makes me think that was a pretty close call -- I found 1 case of that in over 300K LoC! And the problem was because check_func_def() is called for 'def' as well as for 'lambda', and there are various other checks to weed out static methods and to do the right thing for class and instance methods, but somehow a lambda anywhere in a class would also be checked.
(I'm just muttering to myself here, I just think it's interesting to see how time after time we find that there are more combinations of features than we can possibly 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.
For me it's "Lesson 8: The State Explosion Problem" :)
This also assumes foo
in foo = lambda ...
is not a method (from selftype perspective), which I think is fine, but I'd expect to happen once every hundred-thousand lines or 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.
Yeah, foo = lambda ...
is questionable, since when calling it via the class or instance, it will be treated as a method. But you can't have types on lambda args so it shouldn't matter what mypy thinks in that case. :-)
stack = None # type: List[Union[Type, FuncItem, MypyFile]] | ||
|
||
def __init__(self, module: MypyFile) -> None: | ||
self.stack = [module] |
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 don't think the module is ever used. If I replace this with [None]
all tests seem to pass. But I'll just merge now, you can fix this up later.
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 is not used right now, but I don't see how None is more suitable as a sentinel in this case.
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 was the quickest fix -- if I start with an empty stack I have to add guards against an empty stack in active_class(), which currently looks at stack[-1]. But I agree a guard is better.
Fix #2374.
Issues:
The signature fortype.__subclasses__
I suggested does not play well with the check. It can be easily fixed with an appropriateTypeVar
Many incompatibilities with typeshed, Reported in typeshed/#638Added a Scope class, to handle both function_stack and class_context.
@ddfisher