Skip to content

Commit 1d06efa

Browse files
ilevkivskyiMichael0x2a
authored andcommitted
Bounded/constrained type variables don't shadow Any in overloads (#5506)
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.
1 parent 2b246dd commit 1d06efa

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
lines changed

mypy/checker.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3772,7 +3772,17 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo
37723772
37733773
Assumes that both signatures have overlapping argument counts.
37743774
"""
3775-
return is_callable_compatible(signature, other,
3775+
# The extra erasure is needed to prevent spurious errors
3776+
# in situations where an `Any` overload is used as a fallback
3777+
# for an overload with type variables. The spurious error appears
3778+
# because the type variables turn into `Any` during unification in
3779+
# the below subtype check and (surprisingly?) `is_proper_subtype(Any, Any)`
3780+
# returns `True`.
3781+
# TODO: find a cleaner solution instead of this ad-hoc erasure.
3782+
exp_signature = expand_type(signature, {tvar.id: tvar.erase_to_union_or_bound()
3783+
for tvar in signature.variables})
3784+
assert isinstance(exp_signature, CallableType)
3785+
return is_callable_compatible(exp_signature, other,
37763786
is_compat=is_more_precise,
37773787
ignore_return=True)
37783788

mypy/subtypes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,8 +1111,8 @@ def check_argument(leftarg: Type, rightarg: Type, variance: int) -> bool:
11111111
def visit_type_var(self, left: TypeVarType) -> bool:
11121112
if isinstance(self.right, TypeVarType) and left.id == self.right.id:
11131113
return True
1114-
if left.values and is_subtype(UnionType.make_simplified_union(left.values), self.right,
1115-
ignore_promotions=self.ignore_promotions):
1114+
if left.values and self._is_proper_subtype(UnionType.make_simplified_union(left.values),
1115+
self.right):
11161116
return True
11171117
return self._is_proper_subtype(left.upper_bound, self.right)
11181118

mypy/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ def new_unification_variable(old: 'TypeVarDef') -> 'TypeVarDef':
156156
return TypeVarDef(old.name, old.fullname, new_id, old.values,
157157
old.upper_bound, old.variance, old.line, old.column)
158158

159+
def erase_to_union_or_bound(self) -> Type:
160+
if self.values:
161+
return UnionType.make_simplified_union(self.values)
162+
else:
163+
return self.upper_bound
164+
159165
def __repr__(self) -> str:
160166
if self.values:
161167
return '{} in {}'.format(self.name, tuple(self.values))

test-data/unit/check-overloading.test

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4310,3 +4310,67 @@ def f() -> None:
43104310

43114311
[builtins fixtures/dict.pyi]
43124312
[out]
4313+
4314+
[case testOverloadConstrainedTypevarNotShadowingAny]
4315+
from lib import attr
4316+
from typing import Any
4317+
4318+
reveal_type(attr(1)) # E: Revealed type is 'builtins.int*'
4319+
reveal_type(attr("hi")) # E: Revealed type is 'builtins.int'
4320+
x: Any
4321+
reveal_type(attr(x)) # E: Revealed type is 'Any'
4322+
attr("hi", 1) # E: No overload variant of "attr" matches argument types "str", "int" \
4323+
# N: Possible overload variant: \
4324+
# N: def [T in (int, float)] attr(default: T = ..., blah: int = ...) -> T \
4325+
# N: <1 more non-matching overload not shown>
4326+
[file lib.pyi]
4327+
from typing import overload, Any, TypeVar
4328+
4329+
T = TypeVar('T', int, float)
4330+
4331+
@overload
4332+
def attr(default: T = ..., blah: int = ...) -> T: ...
4333+
@overload
4334+
def attr(default: Any = ...) -> int: ...
4335+
[out]
4336+
4337+
[case testOverloadBoundedTypevarNotShadowingAny]
4338+
from lib import attr
4339+
from typing import Any
4340+
4341+
reveal_type(attr(1)) # E: Revealed type is 'builtins.int*'
4342+
reveal_type(attr("hi")) # E: Revealed type is 'builtins.int'
4343+
x: Any
4344+
reveal_type(attr(x)) # E: Revealed type is 'Any'
4345+
attr("hi", 1) # E: No overload variant of "attr" matches argument types "str", "int" \
4346+
# N: Possible overload variant: \
4347+
# N: def [T <: int] attr(default: T = ..., blah: int = ...) -> T \
4348+
# N: <1 more non-matching overload not shown>
4349+
[file lib.pyi]
4350+
from typing import overload, TypeVar, Any
4351+
4352+
T = TypeVar('T', bound=int)
4353+
4354+
@overload
4355+
def attr(default: T = ..., blah: int = ...) -> T: ...
4356+
@overload
4357+
def attr(default: Any = ...) -> int: ...
4358+
[out]
4359+
4360+
[case testAnyIsOKAsFallbackInOverloads]
4361+
import stub
4362+
[file stub.pyi]
4363+
from typing import TypeVar, Any, overload
4364+
4365+
T = TypeVar('T')
4366+
4367+
@overload
4368+
def foo(x: T) -> T: ...
4369+
@overload
4370+
def foo(x: Any) -> Any: ...
4371+
4372+
@overload
4373+
def bar(x: T) -> T: ...
4374+
@overload
4375+
def bar(x: Any) -> int: ...
4376+
[out]

0 commit comments

Comments
 (0)