-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Conversation
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 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).
test-data/unit/semanal-errors.test
Outdated
@@ -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 |
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.
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): |
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.
Since test code never gets executed, you might as well leave the body 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.
Copy that.
[builtins fixtures/isinstance.pyi] | ||
|
||
[case testTypeCheckOverloadWithImplementationFastparse] | ||
# flags: --fast-parser |
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.
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.)
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.
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") |
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.
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
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.
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). |
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.
Hmm... Refactor this to merge with the nearly identical block in the previous method?
mypy/fastparse.py
Outdated
@@ -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 |
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 update this comment a bit? It's still technically correct but less than before...
mypy/fastparse.py
Outdated
and stmt.name() is not None): | ||
ret.append(OverloadedFuncDef(current_overload, stmt)) | ||
current_overload = [] | ||
current_overload_name = None | ||
else: |
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.
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.
mypy/sharedparse.py
Outdated
|
||
|
||
def _is_overload_decorator(dec): | ||
if isinstance(dec, NameExpr) and dec.name in {'overload', 'property', 'abstractproperty'}: |
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 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
?
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.
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 ' |
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.
"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) |
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 feel this file should also grow some additional tests that show the representation of an overload with implementation.
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.
They've stopped being interesting in the parse phase, but the semantic analysis phase files now have such tests.
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.
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.
Ok, I think this is now mostly updated to reflect these comments.
mypy/sharedparse.py
Outdated
|
||
|
||
def _is_overload_decorator(dec): | ||
if isinstance(dec, NameExpr) and dec.name in {'overload', 'property', 'abstractproperty'}: |
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.
Instead I deferred it to semanalysis time, where it belongs
def f(x: 'B') -> 'A': ... | ||
|
||
def f(x: Any) -> Any: | ||
if isinstance(x, 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.
Copy that.
[builtins fixtures/isinstance.pyi] | ||
|
||
[case testTypeCheckOverloadWithImplementationFastparse] | ||
# flags: --fast-parser |
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.
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") |
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.
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) |
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.
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 * |
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 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.
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. |
@sixolet Hm, Appveyor build failed due to |
Well, I just implemented the syntax for inline errors in non-main files. I've already got another commit prepped that I theorize will process them right, though I don't have a Windows box on hand. I'll post it in a sec, once my local run finishes.
… On Feb 17, 2017, at 9:45 AM, Ivan Levkivskyi ***@***.***> wrote:
@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?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
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 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 |
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 really weird. Why are the error messages not in line-number order? (I thought there was some sorting in errors.py???)
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 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 |
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'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) |
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 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.
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.
Thank you, adding a test and a fix. The fix treats an Any
-typed implementation as fine no matter what.
mypy/fastparse.py
Outdated
for stmt in stmts: | ||
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name: | ||
if (isinstance(stmt, Decorator) | ||
and stmt.name() == current_overload_name): |
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 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.
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.
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.
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, it gives you the redefinition error message, correctly)
mypy/fastparse.py
Outdated
current_overload.append(stmt) | ||
elif (isinstance(stmt, FuncDef) | ||
and stmt.name() == current_overload_name | ||
and stmt.name() is not 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.
This would make more sense as checking whether current_overload_name is not None before even calling isinstance() or comparing stmt.name() to 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.
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 " |
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 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.
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.
Done.
mypy/semanal.py
Outdated
t = [] # type: List[CallableType] | ||
non_overload_indexes = [] |
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 this variable should have "error" somewhere in its name?
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 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", |
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.
Either remove "a" or add "an" before "implementation".
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.
Done
mypy/semanal.py
Outdated
"Overload outside a stub must have implementation", | ||
defn) | ||
|
||
if t: |
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 think it's time for t
to get a longer name.
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.
types
sem.function_stack.append(impl.func) | ||
sem.errors.push_function(func.name()) | ||
sem.enter() | ||
impl.func.body.accept(self) |
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 think this deserves at least an else: assert False
since otherwise the stack management would be out of sync.
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.
Done
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.
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) |
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.
Thank you, adding a test and a fix. The fix treats an Any
-typed implementation as fine no matter what.
mypy/fastparse.py
Outdated
for stmt in stmts: | ||
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name: | ||
if (isinstance(stmt, Decorator) | ||
and stmt.name() == current_overload_name): |
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.
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.
mypy/fastparse.py
Outdated
for stmt in stmts: | ||
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name: | ||
if (isinstance(stmt, Decorator) | ||
and stmt.name() == current_overload_name): |
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, it gives you the redefinition error message, correctly)
mypy/fastparse.py
Outdated
current_overload.append(stmt) | ||
elif (isinstance(stmt, FuncDef) | ||
and stmt.name() == current_overload_name | ||
and stmt.name() is not 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.
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']: |
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.
... 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 " |
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.
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", |
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.
Done
mypy/semanal.py
Outdated
"Overload outside a stub must have implementation", | ||
defn) | ||
|
||
if t: |
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.
types
sem.function_stack.append(impl.func) | ||
sem.errors.push_function(func.name()) | ||
sem.enter() | ||
impl.func.body.accept(self) |
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.
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 |
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 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.
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.
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. | ||
|
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 blank line should not be here.
class B: pass | ||
[builtins fixtures/isinstance.pyi] | ||
|
||
|
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.
One blank line is enough.
class B: pass | ||
[builtins fixtures/isinstance.pyi] | ||
|
||
[case testTypeCheckOverloadWithDecoratedImplementation] |
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.
How about another test that checks for errors where the implementation doesn't match the overloads.
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 two so far.
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 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 |
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.
Do you need this also for the implementation? Maybe move this into the else:
block at line 470?
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.
But then why is it not set for non-decorated implementations? Something's still fishy.
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.
Never mind, I was wrong, confusing item.is_overload
and item.func.is_overload
. You're right.
Yee-haw! Now we're just waiting for a clean run of mypy against our internal codebases. |
Yee-haw! Thanks so much Naomi. |
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 |
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/. |
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.
This should fix #1136
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.