Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 74 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Expression type checker. This file is conceptually part of TypeChecker."""

from collections import OrderedDict
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional, Iterable

from mypy.errors import report_internal_error
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any, set_any_tvars
Expand Down Expand Up @@ -1076,8 +1076,10 @@ def overload_call_target(self, arg_types: List[Type], arg_kinds: List[int],
if similarity > 0 and similarity >= best_match:
if (match and not is_same_type(match[-1].ret_type,
typ.ret_type) and
not mypy.checker.is_more_precise_signature(
match[-1], typ)):
(not mypy.checker.is_more_precise_signature(match[-1], typ)
or (any(isinstance(arg, AnyType) for arg in arg_types)
and any_arg_causes_overload_ambiguity(
match + [typ], arg_types, arg_kinds, arg_names)))):
# Ambiguous return type. Either the function overload is
# overlapping (which we don't handle very well here) or the
# caller has provided some Any argument types; in either
Expand Down Expand Up @@ -2764,3 +2766,72 @@ def overload_arg_similarity(actual: Type, formal: Type) -> int:
return 2
# Fall back to a conservative equality check for the remaining kinds of type.
return 2 if is_same_type(erasetype.erase_type(actual), erasetype.erase_type(formal)) else 0


def any_arg_causes_overload_ambiguity(items: List[CallableType],
arg_types: List[Type],
arg_kinds: List[int],
arg_names: List[Optional[str]]) -> bool:
"""May an Any actual argument cause ambiguous result type on call to overloaded function?

Note that this sometimes returns True even if there is no ambiguity, since a correct
implementation would be complex (and the call would be imprecisely typed due to Any
types anyway).

Args:
items: Overload items matching the actual arguments
arg_types: Actual argument types
arg_kinds: Actual argument kinds
arg_names: Actual argument names
"""
actual_to_formal = [
map_formals_to_actuals(
arg_kinds, arg_names, item.arg_kinds, item.arg_names, lambda i: arg_types[i])
for item in items
]

for arg_idx, arg_type in enumerate(arg_types):
if isinstance(arg_type, AnyType):
matching_formals_unfiltered = [(item_idx, lookup[arg_idx])
for item_idx, lookup in enumerate(actual_to_formal)
if lookup[arg_idx]]
matching_formals = []
for item_idx, formals in matching_formals_unfiltered:
if len(formals) > 1:
# An actual maps to multiple formals -- give up as too
# complex, just assume it overlaps.
return True
matching_formals.append((item_idx, items[item_idx].arg_types[formals[0]]))
if (not all_same_types(t for _, t in matching_formals) and
not all_same_types(items[idx].ret_type
for idx, _ in matching_formals)):
# Any maps to multiple different types, and the return types of these items differ.
return True
return False


def all_same_types(types: Iterable[Type]) -> bool:
types = list(types)
if len(types) == 0:
return True
return all(is_same_type(t, types[0]) for t in types[1:])


def map_formals_to_actuals(caller_kinds: List[int],
caller_names: List[Optional[str]],
callee_kinds: List[int],
callee_names: List[Optional[str]],
caller_arg_type: Callable[[int],
Type]) -> List[List[int]]:
"""Calculate the reverse mapping of map_actuals_to_formals."""
formal_to_actual = map_actuals_to_formals(caller_kinds,
caller_names,
callee_kinds,
callee_names,
caller_arg_type)
# Now reverse the mapping.
actual_to_formal = [[] for _ in caller_kinds] # type: List[List[int]]
for formal, actuals in enumerate(formal_to_actual):
for actual in actuals:
actual_to_formal[actual].append(formal)
return actual_to_formal
115 changes: 115 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -1157,3 +1157,118 @@ class Test(object):

t = Test()
reveal_type(t.do_chain) # E: Revealed type is '__main__.Chain'

[case testOverloadWithOverlappingItemsAndAnyArgument1]
from typing import overload, Any

@overload
def f(x: int) -> int: ...
@overload
def f(x: object) -> object: ...
def f(x): pass

a: Any
reveal_type(f(a)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument2]
from typing import overload, Any

@overload
def f(x: int) -> int: ...
@overload
def f(x: float) -> float: ...
def f(x): pass

a: Any
reveal_type(f(a)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument3]
from typing import overload, Any

@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
def f(x): pass

a: Any
reveal_type(f(a)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument4]
from typing import overload, Any

@overload
def f(x: int, y: int, z: str) -> int: ...
@overload
def f(x: object, y: int, z: str) -> object: ...
def f(x): pass

a: Any
# Any causes ambiguity
reveal_type(f(a, 1, '')) # E: Revealed type is 'Any'
# Any causes no ambiguity
reveal_type(f(1, a, a)) # E: Revealed type is 'builtins.int'
reveal_type(f('', a, a)) # E: Revealed type is 'builtins.object'
# Like above, but use keyword arguments.
reveal_type(f(y=1, z='', x=a)) # E: Revealed type is 'Any'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would add one more test where the order of arguments is shuffled, just to be sure that the matching logic works right.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added another call where arguments are shuffled

reveal_type(f(y=a, z='', x=1)) # E: Revealed type is 'builtins.int'
reveal_type(f(z='', x=1, y=a)) # E: Revealed type is 'builtins.int'
reveal_type(f(z='', x=a, y=1)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument5]
from typing import overload, Any, Union

@overload
def f(x: int) -> int: ...
@overload
def f(x: Union[int, float]) -> float: ...
def f(x): pass

a: Any
reveal_type(f(a)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument6]
from typing import overload, Any

@overload
def f(x: int, y: int) -> int: ...
@overload
def f(x: float, y: int, z: str) -> float: ...
@overload
def f(x: object, y: int, z: str, a: None) -> object: ...
def f(x): pass

a: Any
# Any causes ambiguity
reveal_type(f(*a)) # E: Revealed type is 'Any'
reveal_type(f(a, *a)) # E: Revealed type is 'Any'
reveal_type(f(1, *a)) # E: Revealed type is 'Any'
reveal_type(f(1.1, *a)) # E: Revealed type is 'Any'
reveal_type(f('', *a)) # E: Revealed type is 'builtins.object'

[case testOverloadWithOverlappingItemsAndAnyArgument7]
from typing import overload, Any

@overload
def f(x: int, y: int, z: int) -> int: ...
@overload
def f(x: object, y: int, z: int) -> object: ...
def f(x): pass

a: Any
# TODO: We could infer 'int' here
reveal_type(f(1, *a)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument8]
from typing import overload, Any

@overload
def f(x: int, y: int, z: int) -> str: ...
@overload
def f(x: object, y: int, z: int) -> str: ...
def f(x): pass

a: Any
# The return type is not ambiguous so Any arguments cause no ambiguity.
reveal_type(f(a, 1, 1)) # E: Revealed type is 'builtins.str'
reveal_type(f(1, *a)) # E: Revealed type is 'builtins.str'