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
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from mypy import messages
from mypy.subtypes import (
is_subtype, is_equivalent, is_proper_subtype, is_more_precise,
restrict_subtype_away, is_subtype_ignoring_tvars
restrict_subtype_away, is_subtype_ignoring_tvars, is_callable_subtype
)
from mypy.maptype import map_instance_to_supertype
from mypy.typevars import fill_typevars, has_no_typevars
Expand Down Expand Up @@ -261,29 +261,62 @@ def accept_loop(self, body: Statement, else_body: Statement = None, *,

def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
num_abstract = 0
if not defn.items:
# In this case we have already complained about none of these being
# valid overloads.
return None
if len(defn.items) == 1:
self.fail('Single overload definition, multiple required', defn)

if defn.is_property:
# HACK: Infer the type of the property.
self.visit_decorator(defn.items[0])
self.visit_decorator(cast(Decorator, defn.items[0]))
for fdef in defn.items:
assert isinstance(fdef, Decorator)
self.check_func_item(fdef.func, name=fdef.func.name())
if fdef.func.is_abstract:
num_abstract += 1
if num_abstract not in (0, len(defn.items)):
self.fail(messages.INCONSISTENT_ABSTRACT_OVERLOAD, defn)
if defn.impl:
defn.impl.accept(self)
if defn.info:
self.check_method_override(defn)
self.check_inplace_operator_method(defn)
self.check_overlapping_overloads(defn)
return None

def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
# At this point we should have set the impl already, and all remaining
# items are decorators
for i, item in enumerate(defn.items):
assert isinstance(item, Decorator)
sig1 = self.function_type(item.func)
for j, item2 in enumerate(defn.items[i + 1:]):
# TODO overloads involving decorators
sig1 = self.function_type(item.func)
assert isinstance(item2, Decorator)
sig2 = self.function_type(item2.func)
if is_unsafe_overlapping_signatures(sig1, sig2):
self.msg.overloaded_signatures_overlap(i + 1, i + j + 2,
item.func)
if defn.impl:
if isinstance(defn.impl, FuncDef):
impl_type = defn.impl.type
elif isinstance(defn.impl, Decorator):
impl_type = defn.impl.var.type
else:
assert False, "Impl isn't the right type"
# This can happen if we've got an overload with a different
# decorator too -- we gave up on the types.
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.

assert isinstance(sig1, CallableType)
if not is_callable_subtype(impl_type, sig1, ignore_return=True):
self.msg.overloaded_signatures_arg_specific(i + 1, defn.impl)
if not is_subtype(sig1.ret_type, impl_type.ret_type):
self.msg.overloaded_signatures_ret_specific(i + 1, defn.impl)

# Here's the scoop about generators and coroutines.
#
Expand Down Expand Up @@ -2072,7 +2105,8 @@ def check_incompatible_property_override(self, e: Decorator) -> None:
continue
if (isinstance(base_attr.node, OverloadedFuncDef) and
base_attr.node.is_property and
base_attr.node.items[0].var.is_settable_property):
cast(Decorator,
base_attr.node.items[0]).var.is_settable_property):
self.fail(messages.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, e)

def visit_with_stmt(self, s: WithStmt) -> None:
Expand Down
3 changes: 2 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def analyze_member_access(name: str,
if method:
if method.is_property:
assert isinstance(method, OverloadedFuncDef)
return analyze_var(name, method.items[0].var, typ, info, node, is_lvalue, msg,
first_item = cast(Decorator, method.items[0])
return analyze_var(name, first_item.var, typ, info, node, is_lvalue, msg,
original_type, not_ready_callback)
if is_lvalue:
msg.cant_assign_to_method(node)
Expand Down
19 changes: 14 additions & 5 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import sys

from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, cast, List, Set
from mypy.sharedparse import special_function_elide_names, argument_elide_name
from mypy.sharedparse import (
special_function_elide_names, argument_elide_name,
)
from mypy.nodes import (
MypyFile, Node, ImportBase, Import, ImportAll, ImportFrom, FuncDef, OverloadedFuncDef,
MypyFile, Node, ImportBase, Import, ImportAll, ImportFrom, FuncDef,
OverloadedFuncDef, OverloadPart,
ClassDef, Decorator, Block, Var, OperatorAssignmentStmt,
ExpressionStmt, AssignmentStmt, ReturnStmt, RaiseStmt, AssertStmt,
DelStmt, BreakStmt, ContinueStmt, PassStmt, GlobalDecl,
Expand Down Expand Up @@ -211,12 +214,18 @@ def as_block(self, stmts: List[ast3.stmt], lineno: int) -> Block:

def fix_function_overloads(self, stmts: List[Statement]) -> List[Statement]:
ret = [] # type: List[Statement]
current_overload = []
current_overload = [] # type: List[OverloadPart]
current_overload_name = None
# mypy doesn't actually check that the decorator is literally @overload
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.

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.

if len(current_overload) == 1:
ret.append(current_overload[0])
Expand Down
18 changes: 13 additions & 5 deletions mypy/fastparse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import sys

from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, cast, List, Set
from mypy.sharedparse import special_function_elide_names, argument_elide_name
from mypy.sharedparse import (
special_function_elide_names, argument_elide_name,
)
from mypy.nodes import (
MypyFile, Node, ImportBase, Import, ImportAll, ImportFrom, FuncDef, OverloadedFuncDef,
ClassDef, Decorator, Block, Var, OperatorAssignmentStmt,
Expand All @@ -31,7 +33,7 @@
UnaryExpr, FuncExpr, ComparisonExpr, DictionaryComprehension,
SetComprehension, ComplexExpr, EllipsisExpr, YieldExpr, Argument,
Expression, Statement, BackquoteExpr, PrintStmt, ExecStmt,
ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_STAR2
ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_STAR2, OverloadPart,
)
from mypy.types import (
Type, CallableType, AnyType, UnboundType, EllipsisType
Expand Down Expand Up @@ -218,12 +220,18 @@ def as_block(self, stmts: List[ast27.stmt], lineno: int) -> Block:

def fix_function_overloads(self, stmts: List[Statement]) -> List[Statement]:
ret = [] # type: List[Statement]
current_overload = []
current_overload = [] # type: List[OverloadPart]
current_overload_name = None
# mypy doesn't actually check that the decorator is literally @overload
for stmt in stmts:
if isinstance(stmt, Decorator) and stmt.name() == current_overload_name:
if (isinstance(stmt, Decorator)
and stmt.name() == current_overload_name):
current_overload.append(stmt)
elif (isinstance(stmt, FuncDef)
and stmt.name() == current_overload_name
and stmt.name() is not None):
ret.append(OverloadedFuncDef(current_overload + [stmt]))
current_overload = []
current_overload_name = None
else:
if len(current_overload) == 1:
ret.append(current_overload[0])
Expand Down
2 changes: 2 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
o.type.accept(self.type_fixer)
for item in o.items:
item.accept(self)
if o.impl:
o.impl.accept(self)

def visit_decorator(self, d: Decorator) -> None:
if self.current_info is not None:
Expand Down
8 changes: 8 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,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 does not accept all possible arguments '
'of signature {}'.format(index1), context)

def overloaded_signatures_ret_specific(self, index1: int, context: Context) -> None:
self.fail('Overloaded function implementation cannot produce return type '
'of signature {}'.format(index1), context)

def operator_method_signatures_overlap(
self, reverse_class: str, reverse_method: str, forward_class: str,
forward_method: str, context: Context) -> None:
Expand Down
25 changes: 21 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,21 +368,26 @@ def fullname(self) -> str:
return self._fullname


OverloadPart = Union['FuncDef', 'Decorator']


class OverloadedFuncDef(FuncBase, SymbolNode, Statement):
"""A logical node representing all the variants of an overloaded function.

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

items = None # type: List[Decorator]
items = None # type: List[OverloadPart]
impl = None # type: Optional[OverloadPart]

def __init__(self, items: List['Decorator']) -> None:
def __init__(self, items: List['OverloadPart']) -> None:
self.items = items
self.impl = None
self.set_line(items[0].line)

def name(self) -> str:
return self.items[0].func.name()
return self.items[0].name()

def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_overloaded_func_def(self)
Expand All @@ -393,12 +398,17 @@ def serialize(self) -> JsonDict:
'type': None if self.type is None else self.type.serialize(),
'fullname': self._fullname,
'is_property': self.is_property,
'impl': None if self.impl is None else self.impl.serialize()
}

@classmethod
def deserialize(cls, data: JsonDict) -> 'OverloadedFuncDef':
assert data['.class'] == 'OverloadedFuncDef'
res = OverloadedFuncDef([Decorator.deserialize(d) for d in data['items']])
res = OverloadedFuncDef([
cast(OverloadPart, SymbolNode.deserialize(d))
for d in data['items']])
if data.get('impl') is not None:
res.impl = cast(OverloadPart, SymbolNode.deserialize(data['impl']))
if data.get('type') is not None:
res.type = mypy.types.Type.deserialize(data['type'])
res._fullname = data['fullname']
Expand Down Expand Up @@ -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
Member

Choose a reason for hiding this comment

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

Where is this used? I only see this definition (and the one below) but no call sites.

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.

return self.body

def serialize(self) -> JsonDict:
# We're deliberating omitting arguments and storing only arg_names and
# arg_kinds for space-saving reasons (arguments is not used in later
Expand Down Expand Up @@ -582,6 +595,7 @@ class Decorator(SymbolNode, Statement):
func = None # type: FuncDef # Decorated function
decorators = None # type: List[Expression] # Decorators, at least one # XXX Not true
var = None # type: Var # Represents the decorated function obj
type = None # type: mypy.types.Type
is_overload = False

def __init__(self, func: FuncDef, decorators: List[Expression],
Expand All @@ -594,6 +608,9 @@ def __init__(self, func: FuncDef, decorators: List[Expression],
def name(self) -> str:
return self.func.name()

def get_body(self) -> Optional['Block']:
return self.func.body

def fullname(self) -> str:
return self.func.fullname()

Expand Down
19 changes: 11 additions & 8 deletions mypy/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
UnicodeLit, FloatLit, Op, Indent, Keyword, Punct, LexError, ComplexLit,
Copy link
Member

Choose a reason for hiding this comment

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

This file has disappeared, see #2977.

EllipsisToken
)
from mypy.sharedparse import special_function_elide_names, argument_elide_name
from mypy.sharedparse import (
special_function_elide_names, argument_elide_name,
)
from mypy.nodes import (
MypyFile, Import, ImportAll, ImportFrom, FuncDef, OverloadedFuncDef,
ClassDef, Decorator, Block, Var, OperatorAssignmentStmt, Statement,
Expand Down Expand Up @@ -900,14 +902,15 @@ def parse_block(self, allow_type: bool = False) -> Tuple[Block, Type]:
return node, type

def try_combine_overloads(self, s: Statement, stmt: List[Statement]) -> bool:
if isinstance(s, Decorator) and stmt:
fdef = s
n = fdef.func.name()
if isinstance(stmt[-1], Decorator) and stmt[-1].func.name() == n:
stmt[-1] = OverloadedFuncDef([stmt[-1], fdef])
if isinstance(s, (FuncDef, Decorator)) and stmt:
n = s.name()
last = stmt[-1]
if (isinstance(last, (FuncDef, Decorator))
and last.name() == n):
stmt[-1] = OverloadedFuncDef([last, s])
return True
elif isinstance(stmt[-1], OverloadedFuncDef) and stmt[-1].name() == n:
stmt[-1].items.append(fdef)
elif isinstance(last, OverloadedFuncDef) and last.name() == n:
last.items.append(s)
return True
return False

Expand Down
Loading