Skip to content

Commit

Permalink
Bounded/constrained type variables don't shadow Any in overloads (#5506)
Browse files Browse the repository at this point in the history
Fixes #4227

The problem is that after unification with `Any` type variables can also become `Any`. I am aware that this may introduce (rare) cases when we don't detect never-matched overload. However, since this error does not introduce any unsafety, false negatives are clearly better than false positives.
  • Loading branch information
ilevkivskyi authored and Michael0x2a committed Aug 26, 2018
1 parent 2b246dd commit 1d06efa
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 3 deletions.
12 changes: 11 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3772,7 +3772,17 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo
Assumes that both signatures have overlapping argument counts.
"""
return is_callable_compatible(signature, other,
# The extra erasure is needed to prevent spurious errors
# in situations where an `Any` overload is used as a fallback
# for an overload with type variables. The spurious error appears
# because the type variables turn into `Any` during unification in
# the below subtype check and (surprisingly?) `is_proper_subtype(Any, Any)`
# returns `True`.
# TODO: find a cleaner solution instead of this ad-hoc erasure.
exp_signature = expand_type(signature, {tvar.id: tvar.erase_to_union_or_bound()
for tvar in signature.variables})
assert isinstance(exp_signature, CallableType)
return is_callable_compatible(exp_signature, other,
is_compat=is_more_precise,
ignore_return=True)

Expand Down
4 changes: 2 additions & 2 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,8 +1111,8 @@ def check_argument(leftarg: Type, rightarg: Type, variance: int) -> bool:
def visit_type_var(self, left: TypeVarType) -> bool:
if isinstance(self.right, TypeVarType) and left.id == self.right.id:
return True
if left.values and is_subtype(UnionType.make_simplified_union(left.values), self.right,
ignore_promotions=self.ignore_promotions):
if left.values and self._is_proper_subtype(UnionType.make_simplified_union(left.values),
self.right):
return True
return self._is_proper_subtype(left.upper_bound, self.right)

Expand Down
6 changes: 6 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ def new_unification_variable(old: 'TypeVarDef') -> 'TypeVarDef':
return TypeVarDef(old.name, old.fullname, new_id, old.values,
old.upper_bound, old.variance, old.line, old.column)

def erase_to_union_or_bound(self) -> Type:
if self.values:
return UnionType.make_simplified_union(self.values)
else:
return self.upper_bound

def __repr__(self) -> str:
if self.values:
return '{} in {}'.format(self.name, tuple(self.values))
Expand Down
64 changes: 64 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -4310,3 +4310,67 @@ def f() -> None:

[builtins fixtures/dict.pyi]
[out]

[case testOverloadConstrainedTypevarNotShadowingAny]
from lib import attr
from typing import Any

reveal_type(attr(1)) # E: Revealed type is 'builtins.int*'
reveal_type(attr("hi")) # E: Revealed type is 'builtins.int'
x: Any
reveal_type(attr(x)) # E: Revealed type is 'Any'
attr("hi", 1) # E: No overload variant of "attr" matches argument types "str", "int" \
# N: Possible overload variant: \
# N: def [T in (int, float)] attr(default: T = ..., blah: int = ...) -> T \
# N: <1 more non-matching overload not shown>
[file lib.pyi]
from typing import overload, Any, TypeVar

T = TypeVar('T', int, float)

@overload
def attr(default: T = ..., blah: int = ...) -> T: ...
@overload
def attr(default: Any = ...) -> int: ...
[out]

[case testOverloadBoundedTypevarNotShadowingAny]
from lib import attr
from typing import Any

reveal_type(attr(1)) # E: Revealed type is 'builtins.int*'
reveal_type(attr("hi")) # E: Revealed type is 'builtins.int'
x: Any
reveal_type(attr(x)) # E: Revealed type is 'Any'
attr("hi", 1) # E: No overload variant of "attr" matches argument types "str", "int" \
# N: Possible overload variant: \
# N: def [T <: int] attr(default: T = ..., blah: int = ...) -> T \
# N: <1 more non-matching overload not shown>
[file lib.pyi]
from typing import overload, TypeVar, Any

T = TypeVar('T', bound=int)

@overload
def attr(default: T = ..., blah: int = ...) -> T: ...
@overload
def attr(default: Any = ...) -> int: ...
[out]

[case testAnyIsOKAsFallbackInOverloads]
import stub
[file stub.pyi]
from typing import TypeVar, Any, overload

T = TypeVar('T')

@overload
def foo(x: T) -> T: ...
@overload
def foo(x: Any) -> Any: ...

@overload
def bar(x: T) -> T: ...
@overload
def bar(x: Any) -> int: ...
[out]

0 comments on commit 1d06efa

Please sign in to comment.