Skip to content

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

Merged
merged 13 commits into from
Nov 2, 2016
Merged

Conversation

elazarg
Copy link
Contributor

@elazarg elazarg commented Oct 29, 2016

Fix #2374.

Issues:

  1. The error message is clunky
  2. The signature for type.__subclasses__ I suggested does not play well with the check. It can be easily fixed with an appropriate TypeVar
  3. Many incompatibilities with typeshed, Reported in typeshed/#638

Added a Scope class, to handle both function_stack and class_context.

@ddfisher

@elazarg elazarg changed the title Sanity checks for declared selftype Sanity checks for declared selftype [depends on typeshed/#638] Oct 30, 2016
return cls()

class D:
def foo(self: str) -> str: # E: The type of self 'builtins.str' is not a supertype its class '__main__.D'
Copy link
Collaborator

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?

Copy link
Member

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.)

Copy link
Member

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.

Copy link
Member

@gvanrossum gvanrossum Oct 31, 2016

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. :-)

Copy link
Member

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'

Copy link
Contributor Author

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 '{}'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing 'of'.

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

__new__ does not allow passing Type[B] for Type[A] although B <: A. This is why testSuperWithNew fails.

@gvanrossum
Copy link
Member

gvanrossum commented Nov 1, 2016 via email

@gvanrossum gvanrossum changed the title Sanity checks for declared selftype [depends on typeshed/#638] Sanity checks for declared selftype Nov 1, 2016
@gvanrossum
Copy link
Member

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?

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

Sure. But note that the problem with __new__ is unrelated.

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

I will try to solve the __new__ problem.

(One of these days I will learn how to rebase)

@gvanrossum
Copy link
Member

But note that the problem with new is unrelated.

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.

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

The current problem with __new__ is unrelated to typeshed, therefore the tests do not pass yet. I have pretty much fixed your bug report, and the tests I have added pass. It's an existing test that fails - testSuperWithNew - for what seems like a strange subtyping problem:

Argument 1 to "__new__" of "A" has incompatible type "B"; expected "A"

Even though class B inherits from A.

@gvanrossum
Copy link
Member

gvanrossum commented Nov 1, 2016 via email

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

Sorry.

@elazarg
Copy link
Contributor Author

elazarg commented Nov 1, 2016

There was an actual type mismatch in testSuperWithNew. This is ready for a review now.

Any suggestions for a better error message? Perhaps there should be a different one for classmethods.

@gvanrossum
Copy link
Member

Thanks, I'll see if I can dive into this once I'm done with another big code review.

Copy link
Member

@gvanrossum gvanrossum left a 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?
Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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]:
Copy link
Member

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:
Copy link
Member

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)
Copy link
Member

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()
Copy link
Member

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:
Copy link
Member

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!

Copy link
Contributor Author

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
Copy link
Member

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.)

Copy link
Contributor Author

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.

Copy link
Member

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]
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

@gvanrossum gvanrossum merged commit a90841f into python:master Nov 2, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mypy doesn't ensure self types are reasonable
3 participants