Skip to content
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

Allow overloads in source files, not just stubs #2603

Merged
merged 18 commits into from
Mar 27, 2017

Conversation

sixolet
Copy link
Collaborator

@sixolet sixolet commented Dec 23, 2016

This should fix #1136

  • The implementation must directly follow all the overloads
  • The implementation is typechecked exactly according to its own declared types
  • Indicates an error if the implementation's argument list is not more general than every override variant
  • Also indicates an error if the implementation's return type is also not more general than the return type of every override variant

It also limits overloads to the decorators that are specifically geared towards providing overload-type functionality -- @overload, but also @property and its cousins @funcname.setter and @funcname.deleter. All other decorators are treated purely as decorators, and now provide redefinition errors if you repeat a function definition with them, instead of errors about an overload you probably did not mean.

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.

I am really excited about this! Here are some nits, nothing major except the missing case of the decorated implementation (which actually needs to be fixed in three places).

@@ -1056,25 +1056,25 @@ from typing import overload
def dec(x): pass
@overload
def f(): pass
@dec # E: 'overload' decorator expected
@dec # E: Name 'f' already defined
Copy link
Member

Choose a reason for hiding this comment

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

We actually prefer two spaces before the # E: -- this follows PEP 8 (there are still many examples with one space from long ago).

def f(x: 'B') -> 'A': ...

def f(x: Any) -> Any:
if isinstance(x, A):
Copy link
Member

Choose a reason for hiding this comment

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

Since test code never gets executed, you might as well leave the body pass.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy that.

[builtins fixtures/isinstance.pyi]

[case testTypeCheckOverloadWithImplementationFastparse]
# flags: --fast-parser
Copy link
Member

Choose a reason for hiding this comment

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

Honestly I would be happy if this only worked with --fast-parser -- we're going to phase out the slow parser soon(-ish) and I don't see the need to double the number of tests for this feature just to ensure it works with both parsers. (However it would be good to also have some PY2 tests.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Turning doubled tests into py2 tests, now that they're fast-parser by default.

def f(x: Any) -> Any:
if isinstance(x, A):
y = x
y = B() # E: Incompatible types in assignment (expression has type "B", variable has type "A")
Copy link
Member

Choose a reason for hiding this comment

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

Is it important that the error refers to the classes used in the overload? Assuming we don't do anything special for checking the body of the implementation, I'd only care about a few things:

  • for a body without annotations, errors are not reported
  • for a body with annotations (even just -> Any), errors are reported as usual

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Simplified the test and then re-complicated it by testing both the things you suggest.

mypy/semanal.py Outdated
@@ -2906,6 +2909,16 @@ def visit_overloaded_func_def(self, func: OverloadedFuncDef) -> None:
func._fullname = self.sem.qualified_name(func.name())
if kind == GDEF:
self.sem.globals[func.name()] = SymbolTableNode(kind, func, self.sem.cur_mod_id)
if func.impl:
# Also analyze the function body (in case there are conditional imports).
Copy link
Member

Choose a reason for hiding this comment

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

Hmm... Refactor this to merge with the nearly identical block in the previous method?

@@ -190,19 +192,27 @@ def as_block(self, stmts: List[ast35.stmt], lineno: int) -> Block:

def fix_function_overloads(self, stmts: List[Statement]) -> List[Statement]:
ret = [] # type: List[Statement]
current_overload = []
current_overload = [] # type: List[Decorator]
current_overload_name = None
# mypy doesn't actually check that the decorator is literally @overload
Copy link
Member

Choose a reason for hiding this comment

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

Maybe update this comment a bit? It's still technically correct but less than before...

and stmt.name() is not None):
ret.append(OverloadedFuncDef(current_overload, stmt))
current_overload = []
current_overload_name = None
else:
Copy link
Member

Choose a reason for hiding this comment

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

Looks like there's a missing case here: a Decorator that's not an overload but does match the current overload name. That would indicate a decorated implementation, which I think we ought to support.



def _is_overload_decorator(dec):
if isinstance(dec, NameExpr) and dec.name in {'overload', 'property', 'abstractproperty'}:
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 add a comment explaining why properties are also considered overloads? Is this just because the same parse tree structure is used to support a sequence of @property, @f.getter and/or @f.deleter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Instead I deferred it to semanalysis time, where it belongs

mypy/messages.py Outdated
@@ -807,6 +807,14 @@ def overloaded_signatures_overlap(self, index1: int, index2: int,
self.fail('Overloaded function signatures {} and {} overlap with '
'incompatible return types'.format(index1, index2), context)

def overloaded_signatures_arg_specific(self, index1: int, context: Context) -> None:
self.fail('Overloaded function implementation cannot accept all possible arguments '
Copy link
Member

Choose a reason for hiding this comment

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

"cannot" -> "does not" or "should". (But the next one is correct IMO.)

FuncDef:3(
g
Block:3(
PassStmt:3())))
Decorator:4(
Var(g)
NameExpr(foo)
NameExpr(overload)
Copy link
Member

Choose a reason for hiding this comment

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

I feel this file should also grow some additional tests that show the representation of an overload with implementation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They've stopped being interesting in the parse phase, but the semantic analysis phase files now have such tests.

jkleint added a commit to jkleint/mypy that referenced this pull request Feb 2, 2017
The docs say `@override` doesn't work in user code, but it seems to work in mypy 0.470.
The update may be waiting on python#2603, but that PR does not seem to include doc updates,
so feel free to put this patch in that PR.
Copy link
Collaborator Author

@sixolet sixolet left a comment

Choose a reason for hiding this comment

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

Ok, I think this is now mostly updated to reflect these comments.



def _is_overload_decorator(dec):
if isinstance(dec, NameExpr) and dec.name in {'overload', 'property', 'abstractproperty'}:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Instead I deferred it to semanalysis time, where it belongs

def f(x: 'B') -> 'A': ...

def f(x: Any) -> Any:
if isinstance(x, A):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy that.

[builtins fixtures/isinstance.pyi]

[case testTypeCheckOverloadWithImplementationFastparse]
# flags: --fast-parser
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Turning doubled tests into py2 tests, now that they're fast-parser by default.

def f(x: Any) -> Any:
if isinstance(x, A):
y = x
y = B() # E: Incompatible types in assignment (expression has type "B", variable has type "A")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Simplified the test and then re-complicated it by testing both the things you suggest.

FuncDef:3(
g
Block:3(
PassStmt:3())))
Decorator:4(
Var(g)
NameExpr(foo)
NameExpr(overload)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They've stopped being interesting in the parse phase, but the semantic analysis phase files now have such tests.

@@ -410,6 +410,8 @@ class B(object, A): pass \
# E: Cannot determine consistent method resolution order (MRO) for "B"

[case testOverloadedAbstractMethod]
from foo import *
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This chicanery is now endemic to the previously-existing tests, to make them all test overloads in stub files, so I don't have to introduce an implementation to each.

@sixolet
Copy link
Collaborator Author

sixolet commented Feb 17, 2017

NB: The rules used to determine whether an implementation is valid for a series of overloads take into account its argument names, the same way function subtyping does. That strictness is technically correct (my favorite kind of correct), but may be annoying.

@ilevkivskyi
Copy link
Member

@sixolet Hm, Appveyor build failed due to / vs \ issue on Windows. Maybe this is related to your extensive use of from foo import * in tests?

@sixolet
Copy link
Collaborator Author

sixolet commented Feb 17, 2017 via email

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.

Can you also merge master? (We're trying out a new policy -- it seems merging is actually preferable for the reviewer over rebasing.)

main:5: error: Name 'f' already defined
main:7: error: Name 'g' already defined
tmp/foo.pyi:7: error: Name 'g' already defined
tmp/foo.pyi:5: error: Name 'f' already defined
Copy link
Member

Choose a reason for hiding this comment

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

This is really weird. Why are the error messages not in line-number order? (I thought there was some sorting in errors.py???)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I spent a while tracking down the reason for it:

  • For objects we initially thought were part of an overload series, but turn out not to be, we give the already-defined error in semantic analysis phase two, now.
  • For objects that are plain redefinitions, we give the already-defined error in phase 1.
  • We were setting an import_context object differently between these two phases
  • Error sorting groups by import_context, not just file.

I've made a change to set import_context the same way between the two phases.

@overload
def f(x: 'B') -> 'A': ...

def f(x: 'A') -> Any: # E: Overloaded function implementation does not accept all possible arguments of signature 2
Copy link
Member

Choose a reason for hiding this comment

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

I'd love to see a line number reference here for the specific signature that we're incompatible with here. I know it's probably hard to do, so maybe in a follow-up PR?

mypy/checker.py Outdated
if impl_type is None or sig1 is None:
return

assert isinstance(impl_type, CallableType)
Copy link
Member

Choose a reason for hiding this comment

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

I can get this to crash. Here's a hint on the repro:

@overload
def f(a: int) -> int: return a+1
@overload
def f(s: str) -> str: return str(int(s) + 1)
@deco
def f(a: Any) -> Any: pass

According to pdb impl_type is Any; defn.impl is a Decorator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, adding a test and a fix. The fix treats an Any-typed implementation as fine no matter what.

for stmt in stmts:
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name:
if (isinstance(stmt, Decorator)
and stmt.name() == current_overload_name):
Copy link
Member

Choose a reason for hiding this comment

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

I think this should double-check that one of the decorators is actually @overload (or @typing.overload).

And if there are decorators but none of them are "overload" and the name matches, this should be considered the implementation.

And there should be tests for that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can't check that here -- we don't know until the semantic analysis step. There's code there to compensate for that.

Making sure we have a test for all the various cases of that and that they give solid error messages.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(Yes, it gives you the redefinition error message, correctly)

current_overload.append(stmt)
elif (isinstance(stmt, FuncDef)
and stmt.name() == current_overload_name
and stmt.name() is not None):
Copy link
Member

Choose a reason for hiding this comment

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

This would make more sense as checking whether current_overload_name is not None before even calling isinstance() or comparing stmt.name() to it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool, yeah.

mypy/semanal.py Outdated
# Some of them were overloads, but not all.
for idx in non_overload_indexes:
if self.is_stub_file:
self.fail("Implementations of overloaded functions "
Copy link
Member

Choose a reason for hiding this comment

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

Why the plural (here and in the next error)? Surely at most one implementation is allowed anyway. I think all three plurals in the sentence should become singulars.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

mypy/semanal.py Outdated
t = [] # type: List[CallableType]
non_overload_indexes = []
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this variable should have "error" somewhere in its name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's not an error as long as the only non-overload is the last one.

mypy/semanal.py Outdated

elif not self.is_stub_file and not non_overload_indexes:
self.fail(
"Overload outside a stub must have implementation",
Copy link
Member

Choose a reason for hiding this comment

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

Either remove "a" or add "an" before "implementation".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

mypy/semanal.py Outdated
"Overload outside a stub must have implementation",
defn)

if t:
Copy link
Member

Choose a reason for hiding this comment

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

I think it's time for t to get a longer name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

types

sem.function_stack.append(impl.func)
sem.errors.push_function(func.name())
sem.enter()
impl.func.body.accept(self)
Copy link
Member

Choose a reason for hiding this comment

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

I think this deserves at least an else: assert False since otherwise the stack management would be out of sync.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
Collaborator Author

@sixolet sixolet left a comment

Choose a reason for hiding this comment

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

Ok, I think I handled most of these comments now

mypy/checker.py Outdated
if impl_type is None or sig1 is None:
return

assert isinstance(impl_type, CallableType)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, adding a test and a fix. The fix treats an Any-typed implementation as fine no matter what.

for stmt in stmts:
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name:
if (isinstance(stmt, Decorator)
and stmt.name() == current_overload_name):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can't check that here -- we don't know until the semantic analysis step. There's code there to compensate for that.

Making sure we have a test for all the various cases of that and that they give solid error messages.

for stmt in stmts:
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name:
if (isinstance(stmt, Decorator)
and stmt.name() == current_overload_name):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(Yes, it gives you the redefinition error message, correctly)

current_overload.append(stmt)
elif (isinstance(stmt, FuncDef)
and stmt.name() == current_overload_name
and stmt.name() is not None):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool, yeah.

mypy/nodes.py Outdated
@@ -535,6 +545,9 @@ def name(self) -> str:
def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_func_def(self)

def get_body(self) -> Optional['Block']:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

... Thank you. Cruft from a previous way of doing it.

mypy/semanal.py Outdated
# Some of them were overloads, but not all.
for idx in non_overload_indexes:
if self.is_stub_file:
self.fail("Implementations of overloaded functions "
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

mypy/semanal.py Outdated

elif not self.is_stub_file and not non_overload_indexes:
self.fail(
"Overload outside a stub must have implementation",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

mypy/semanal.py Outdated
"Overload outside a stub must have implementation",
defn)

if t:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

types

sem.function_stack.append(impl.func)
sem.errors.push_function(func.name())
sem.enter()
impl.func.body.accept(self)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

main:5: error: Name 'f' already defined
main:7: error: Name 'g' already defined
tmp/foo.pyi:7: error: Name 'g' already defined
tmp/foo.pyi:5: error: Name 'f' already defined
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I spent a while tracking down the reason for it:

  • For objects we initially thought were part of an overload series, but turn out not to be, we give the already-defined error in semantic analysis phase two, now.
  • For objects that are plain redefinitions, we give the already-defined error in phase 1.
  • We were setting an import_context object differently between these two phases
  • Error sorting groups by import_context, not just file.

I've made a change to set import_context the same way between the two phases.

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.

Major progress -- I found nothing else except some excess blank lines and a suggestion for a new test. We also discussed over lunch that there should be a bunch of tests involving generic functions in various positions, checking both correct examples and error cases.

mypy/nodes.py Outdated

This node has no explicit representation in the source program.
Overloaded variants must be consecutive in the source file.

Copy link
Member

Choose a reason for hiding this comment

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

This blank line should not be here.

class B: pass
[builtins fixtures/isinstance.pyi]


Copy link
Member

Choose a reason for hiding this comment

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

One blank line is enough.

class B: pass
[builtins fixtures/isinstance.pyi]

[case testTypeCheckOverloadWithDecoratedImplementation]
Copy link
Member

Choose a reason for hiding this comment

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

How about another test that checks for errors where the implementation doesn't match the overloads.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are two so far.

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.

Thanks for adding the new tests! There are still two blank lines I'd like to kill, and on re-reviewing the major recent diff I found one questionable issue.

Otherwise LGTM.

mypy/semanal.py Outdated
item.accept(self)
# TODO support decorated overloaded functions properly
if isinstance(item, Decorator):
item.func.is_overload = True
Copy link
Member

Choose a reason for hiding this comment

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

Do you need this also for the implementation? Maybe move this into the else: block at line 470?

Copy link
Member

Choose a reason for hiding this comment

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

But then why is it not set for non-decorated implementations? Something's still fishy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Never mind, I was wrong, confusing item.is_overload and item.func.is_overload. You're right.

@gvanrossum
Copy link
Member

Yee-haw! Now we're just waiting for a clean run of mypy against our internal codebases.

@gvanrossum gvanrossum merged commit e674e25 into python:master Mar 27, 2017
@gvanrossum
Copy link
Member

Yee-haw! Thanks so much Naomi.

@pkch
Copy link
Contributor

pkch commented Mar 28, 2017

Is it already the time to update the docs (they still say that overloading isn't allowed outside the stubs)? Or we need to wait until it becomes part of an official mypy version?

@ilevkivskyi
Copy link
Member

@pkch FYI there is already a PR #2792 for this.

@gvanrossum
Copy link
Member

Policy is to update the docs in master at the same time as the code, or as soon after as we can (but always before the next release). People wanting docs for the latest released version can go to http://mypy.readthedocs.io/en/stable/.

gvanrossum pushed a commit that referenced this pull request Apr 12, 2017
The docs say `@override` doesn't work in user code, but it seems to work in mypy 0.470.
The update may be waiting on #2603, but that PR does not seem to include doc updates,
so feel free to put this patch in that PR.
gvanrossum added a commit that referenced this pull request Apr 12, 2017
…es) (#3159)

These are the doc updates corresponding to PR #2603 by @sixolet (specified in a revision of PEP 484: https://www.python.org/dev/peps/pep-0484/#function-method-overloading). The docs got started in PR #PR #2792 by @jkleint.
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.

Allow @overload outside stubs
4 participants