Skip to content

Commit

Permalink
Make overload checks more strict when there are multiple 'Any's (#5254)
Browse files Browse the repository at this point in the history
* Make overload checks more strict when there are multiple 'Any's

Resolves #5250

This makes the "multiple overload matches due to Any" even more strict:
we now return a non-Any type only if all of the return types are the
same.

* Make overlaps due to Any revert to using erased types if possible

This change also modifies how mypy erases callables. Previously,
callables of type 'Callable[[A, B, ...], R]' were erased to
'Callable[[], None]'.

This change will now make the erasure be 'Callable[..., Any]', largely
on the grounds that it seems more useful.
  • Loading branch information
Michael0x2a authored and ilevkivskyi committed Jun 21, 2018
1 parent 01adcf4 commit a75fa88
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 17 deletions.
14 changes: 6 additions & 8 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import mypy.checker
from mypy import types
from mypy.sametypes import is_same_type
from mypy.erasetype import replace_meta_vars
from mypy.erasetype import replace_meta_vars, erase_type
from mypy.messages import MessageBuilder
from mypy import messages
from mypy.infer import infer_type_arguments, infer_function_type_arguments
Expand Down Expand Up @@ -1314,14 +1314,12 @@ def infer_overload_return_type(self,
return None
elif any_causes_overload_ambiguity(matches, return_types, arg_types, arg_kinds, arg_names):
# An argument of type or containing the type 'Any' caused ambiguity.
if all(is_subtype(ret_type, return_types[-1]) and
is_subtype(return_types[-1], ret_type)
for ret_type in return_types[:-1]):
# The last match is mutually compatible with all previous ones, so it's safe
# to return that inferred type.
return return_types[-1], inferred_types[-1]
# We try returning a precise type if we can. If not, we give up and just return 'Any'.
if all_same_types(return_types):
return return_types[0], inferred_types[0]
elif all_same_types(erase_type(typ) for typ in return_types):
return erase_type(return_types[0]), erase_type(inferred_types[0])
else:
# We give up and return 'Any'.
return self.check_call(callee=AnyType(TypeOfAny.special_form),
args=args,
arg_kinds=arg_kinds,
Expand Down
20 changes: 14 additions & 6 deletions mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
CallableType, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType,
DeletedType, TypeTranslator, UninhabitedType, TypeType, TypeOfAny
)
from mypy.nodes import ARG_STAR, ARG_STAR2


def erase_type(typ: Type) -> Type:
"""Erase any type variables from a type.
Also replace tuple types with the corresponding concrete types. Replace
callable types with empty callable types.
Also replace tuple types with the corresponding concrete types.
Examples:
A -> A
B[X] -> B[Any]
Tuple[A, B] -> tuple
Callable[...] -> Callable[[], None]
Callable[[A1, A2, ...], R] -> Callable[..., Any]
Type[X] -> Type[Any]
"""

Expand Down Expand Up @@ -57,11 +57,19 @@ def visit_type_var(self, t: TypeVarType) -> Type:

def visit_callable_type(self, t: CallableType) -> Type:
# We must preserve the fallback type for overload resolution to work.
ret_type = NoneTyp() # type: Type
return CallableType([], [], [], ret_type, t.fallback)
any_type = AnyType(TypeOfAny.special_form)
return CallableType(
arg_types=[any_type, any_type],
arg_kinds=[ARG_STAR, ARG_STAR2],
arg_names=[None, None],
ret_type=any_type,
fallback=t.fallback,
is_ellipsis_args=True,
implicit=True,
)

def visit_overloaded(self, t: Overloaded) -> Type:
return t.items()[0].accept(self)
return t.fallback.accept(self)

def visit_tuple_type(self, t: TupleType) -> Type:
return t.fallback.accept(self)
Expand Down
14 changes: 11 additions & 3 deletions mypy/test/testtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
UnboundType, AnyType, CallableType, TupleType, TypeVarDef, Type, Instance, NoneTyp, Overloaded,
TypeType, UnionType, UninhabitedType, true_only, false_only, TypeVarId, TypeOfAny
)
from mypy.nodes import ARG_POS, ARG_OPT, ARG_STAR, CONTRAVARIANT, INVARIANT, COVARIANT
from mypy.nodes import ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, CONTRAVARIANT, INVARIANT, COVARIANT
from mypy.subtypes import is_subtype, is_more_precise, is_proper_subtype
from mypy.test.typefixture import TypeFixture, InterfaceTypeFixture

Expand Down Expand Up @@ -150,11 +150,19 @@ def test_erase_with_tuple_type(self) -> None:

def test_erase_with_function_type(self) -> None:
self.assert_erase(self.fx.callable(self.fx.a, self.fx.b),
self.fx.callable_type(self.fx.nonet))
CallableType(arg_types=[self.fx.anyt, self.fx.anyt],
arg_kinds=[ARG_STAR, ARG_STAR2],
arg_names=[None, None],
ret_type=self.fx.anyt,
fallback=self.fx.function))

def test_erase_with_type_object(self) -> None:
self.assert_erase(self.fx.callable_type(self.fx.a, self.fx.b),
self.fx.callable_type(self.fx.nonet))
CallableType(arg_types=[self.fx.anyt, self.fx.anyt],
arg_kinds=[ARG_STAR, ARG_STAR2],
arg_names=[None, None],
ret_type=self.fx.anyt,
fallback=self.fx.type_type))

def test_erase_with_type_type(self) -> None:
self.assert_erase(self.fx.type_a, self.fx.type_a)
Expand Down
80 changes: 80 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,86 @@ reveal_type(f(**a)) # E: Revealed type is 'Any'

[builtins fixtures/dict.pyi]

[case testOverloadWithOverlappingItemsAndAnyArgument12]
from typing import overload, Any

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

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

[case testOverloadWithOverlappingItemsAndAnyArgument13]
from typing import Any, overload, TypeVar, Generic

class slice: pass

T = TypeVar('T')
class A(Generic[T]):
@overload
def f(self, x: int) -> T: ...
@overload
def f(self, x: slice) -> A[T]: ...
def f(self, x): ...

i: Any
a: A[Any]
reveal_type(a.f(i)) # E: Revealed type is 'Any'

[case testOverloadWithOverlappingItemsAndAnyArgument14]
from typing import Any, overload, TypeVar, Generic

T = TypeVar('T')

class Wrapper(Generic[T]): pass
class slice: pass

class A(Generic[T]):
@overload
def f(self, x: int) -> Wrapper[T]: ...
@overload
def f(self, x: slice) -> Wrapper[A[T]]: ...
def f(self, x): ...

i: Any
a: A[Any]
reveal_type(a.f(i)) # E: Revealed type is '__main__.Wrapper[Any]'

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

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

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

a: Any
reveal_type(f(a)) # E: Revealed type is 'builtins.str'
reveal_type(g(a)) # E: Revealed type is 'Union[builtins.str, builtins.int]'

[case testOverloadWithOverlappingItemsAndAnyArgument16]
from typing import overload, Any, Union, Callable

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

a: Any
reveal_type(f(a)) # E: Revealed type is 'def (*Any, **Any) -> Any'
reveal_type(f(a)(a)) # E: Revealed type is 'Any'

[case testOverloadOnOverloadWithType]
from typing import Any, Type, TypeVar, overload
from mod import MyInt
Expand Down

0 comments on commit a75fa88

Please sign in to comment.