From 968c231f3615293f97c9a0c3615b937a09fa6d60 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Aug 2018 15:56:29 +0100 Subject: [PATCH 1/4] Bounded/constrained type variables don't shadow Any in overloads --- mypy/checker.py | 2 ++ mypy/subtypes.py | 4 +-- mypy/types.py | 11 ++++--- test-data/unit/check-overloading.test | 46 +++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5ba197323820..22c3d961e64a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3752,6 +3752,8 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo Assumes that both signatures have overlapping argument counts. """ + signature = expand_type(signature, {tvar.id: tvar.erase_to_union_or_bound() + for tvar in signature.variables}) return is_callable_compatible(signature, other, is_compat=is_more_precise, ignore_return=True) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a3fb582cb432..3032aff9d061 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -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) diff --git a/mypy/types.py b/mypy/types.py index 19a8d9274d0f..edbca3d68324 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -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)) @@ -561,10 +567,7 @@ def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_type_var(self) def erase_to_union_or_bound(self) -> Type: - if self.values: - return UnionType.make_simplified_union(self.values) - else: - return self.upper_bound + return self.binder.erase_to_union_or_bound() def __hash__(self) -> int: return hash(self.id) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index ef26f7f91484..4e6b22481155 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -4257,3 +4257,49 @@ 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] From 867a6fd2fe3607f4ab52d6ebbe902747b0197698 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Aug 2018 15:59:38 +0100 Subject: [PATCH 2/4] Binder is not stored --- mypy/types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index edbca3d68324..8e8f950b2276 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -567,7 +567,10 @@ def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_type_var(self) def erase_to_union_or_bound(self) -> Type: - return self.binder.erase_to_union_or_bound() + if self.values: + return UnionType.make_simplified_union(self.values) + else: + return self.upper_bound def __hash__(self) -> int: return hash(self.id) From e0046f6cd604b38473d5dc0138fb632b0e48c623 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Aug 2018 16:02:09 +0100 Subject: [PATCH 3/4] Fix self-check --- mypy/checker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 22c3d961e64a..029b8c48e3b6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3752,9 +3752,10 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo Assumes that both signatures have overlapping argument counts. """ - signature = expand_type(signature, {tvar.id: tvar.erase_to_union_or_bound() - for tvar in signature.variables}) - return is_callable_compatible(signature, other, + 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) From e296248a0d74f913162f596215818148c3cf56e3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 26 Aug 2018 21:15:47 +0100 Subject: [PATCH 4/4] Address CR --- mypy/checker.py | 7 +++++++ test-data/unit/check-overloading.test | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 029b8c48e3b6..8edebe8afd5a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3752,6 +3752,13 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo Assumes that both signatures have overlapping argument counts. """ + # 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) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 4e6b22481155..8c50e74b1571 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -4303,3 +4303,21 @@ 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]