From d1ec354516a812b86e97f5a1aeb36dc1bff904af Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Wed, 26 Apr 2017 15:19:33 -0500 Subject: [PATCH 1/5] Fix #3262: allow subtypes to define more overloads than their supertype --- mypy/subtypes.py | 16 +++++++---- test-data/unit/check-classes.test | 44 +++++++++++++++++++------------ 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a280c217cd8a7..9848cb45154b8 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -249,13 +249,19 @@ def visit_overloaded(self, left: Overloaded) -> bool: return True return False elif isinstance(right, Overloaded): - # TODO: this may be too restrictive - if len(left.items()) != len(right.items()): - return False - for i in range(len(left.items())): - if not is_subtype(left.items()[i], right.items()[i], self.check_type_parameter, + # Ensure each overload in the left side is accounted for. + sub_overloads = left.items()[:] + while sub_overloads: + left_item = sub_overloads[-1] + for right_item in right.items(): + if is_subtype(left_item, right_item, self.check_type_parameter, ignore_pos_arg_names=self.ignore_pos_arg_names): + sub_overloads.pop() + break + else: + # One of the overloads was not present in the right side. return False + return True elif isinstance(right, UnboundType): return True diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 1dd3353ec903c..36c66a906001d 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1419,23 +1419,6 @@ class B(A): [out] tmp/foo.pyi:8: error: Signature of "__add__" incompatible with supertype "A" -[case testOverloadedOperatorMethodOverrideWithSwitchedItemOrder] -from foo import * -[file foo.pyi] -from typing import overload, Any -class A: - @overload - def __add__(self, x: 'B') -> 'B': pass - @overload - def __add__(self, x: 'A') -> 'A': pass -class B(A): - @overload - def __add__(self, x: 'A') -> 'A': pass - @overload - def __add__(self, x: 'B') -> 'B': pass -[out] -tmp/foo.pyi:8: error: Signature of "__add__" incompatible with supertype "A" - [case testReverseOperatorMethodArgumentType] from typing import Any class A: pass @@ -2494,6 +2477,33 @@ reveal_type(f(BChild())) # E: Revealed type is 'foo.B' [builtins fixtures/classmethod.pyi] [out] +[case testSubtypeWithMoreOverloadsThanSupertypeSucceeds] +from foo import * +[file foo.pyi] +from typing import overload + + +class X: pass +class Y: pass +class Z: pass + + +class A: + @overload + def f(self, x: X) -> X: pass + @overload + def f(self, y: Y) -> Y: pass + +class B(A): + @overload + def f(self, x: X) -> X: pass + @overload + def f(self, y: Y) -> Y: pass + @overload + def f(self, z: Z) -> Z: pass +[builtins fixtures/classmethod.pyi] +[out] + [case testTypeTypeOverlapsWithObjectAndType] from foo import * [file foo.pyi] From 49e7b229fab278b872dea8c0caa4ff05f2013bb2 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Fri, 15 Sep 2017 15:30:13 -0500 Subject: [PATCH 2/5] Swap subtype check --- mypy/subtypes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 9848cb45154b8..948df497baf33 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -249,17 +249,17 @@ def visit_overloaded(self, left: Overloaded) -> bool: return True return False elif isinstance(right, Overloaded): - # Ensure each overload in the left side is accounted for. - sub_overloads = left.items()[:] - while sub_overloads: - left_item = sub_overloads[-1] - for right_item in right.items(): + # Ensure each overload in the right side is accounted for. + super_overloads = right.items()[:] + while super_overloads: + right_item = super_overloads[-1] + for left_item in left.items(): if is_subtype(left_item, right_item, self.check_type_parameter, ignore_pos_arg_names=self.ignore_pos_arg_names): - sub_overloads.pop() + super_overloads.pop() break else: - # One of the overloads was not present in the right side. + # One of the overloads was not present in the left side. return False return True From b8a83043ce8cf9631b4b9177f076887dadf8a773 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Sat, 30 Sep 2017 17:19:25 -0500 Subject: [PATCH 3/5] Fix an old test --- test-data/unit/check-classes.test | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 36c66a906001d..95c840ad4ff81 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1416,8 +1416,6 @@ class B(A): def __add__(self, x: str) -> A: pass @overload def __add__(self, x: type) -> A: pass -[out] -tmp/foo.pyi:8: error: Signature of "__add__" incompatible with supertype "A" [case testReverseOperatorMethodArgumentType] from typing import Any From 2ca49fcf12eba664da15364626aba32eabbbd34e Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Tue, 7 Nov 2017 14:06:46 -0600 Subject: [PATCH 4/5] Fix several overload supertype/subtype cases --- mypy/subtypes.py | 38 +++++++++++----- test-data/unit/check-classes.test | 72 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 948df497baf33..0dcbf9fa07c54 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -249,17 +249,33 @@ def visit_overloaded(self, left: Overloaded) -> bool: return True return False elif isinstance(right, Overloaded): - # Ensure each overload in the right side is accounted for. - super_overloads = right.items()[:] - while super_overloads: - right_item = super_overloads[-1] - for left_item in left.items(): - if is_subtype(left_item, right_item, self.check_type_parameter, - ignore_pos_arg_names=self.ignore_pos_arg_names): - super_overloads.pop() - break - else: - # One of the overloads was not present in the left side. + # Ensure each overload in the right side (the supertype) is accounted for. + previous_match_left_index = -1 + + for right_index, right_item in enumerate(right.items()): + found_match = False + + for left_index, left_item in enumerate(left.items()): + subtype_match = is_subtype(left_item, right_item, self.check_type_parameter, + ignore_pos_arg_names=self.ignore_pos_arg_names) + + # Order matters: we need to make sure that the index of + # this item is at least the index of the previous one. + if subtype_match and previous_match_left_index <= left_index: + if not found_match: + # Update the index of the previous match. + previous_match_left_index = left_index + found_match = True + else: + # If this one overlaps with the supertype in any way, but it wasn't + # an exact match, then it's a type error. + if (is_callable_subtype(left_item, right_item, ignore_return=True, + ignore_pos_arg_names=self.ignore_pos_arg_names) or + is_callable_subtype(right_item, left_item, ignore_return=True, + ignore_pos_arg_names=self.ignore_pos_arg_names)): + return False + + if not found_match: return False return True diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 95c840ad4ff81..94a74ba2fa69e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1417,6 +1417,23 @@ class B(A): @overload def __add__(self, x: type) -> A: pass +[case testOverloadedOperatorMethodOverrideWithSwitchedItemOrder] +from foo import * +[file foo.pyi] +from typing import overload, Any +class A: + @overload + def __add__(self, x: 'B') -> 'B': pass + @overload + def __add__(self, x: 'A') -> 'A': pass +class B(A): + @overload + def __add__(self, x: 'A') -> 'A': pass + @overload + def __add__(self, x: 'B') -> 'B': pass +[out] +tmp/foo.pyi:8: error: Signature of "__add__" incompatible with supertype "A" + [case testReverseOperatorMethodArgumentType] from typing import Any class A: pass @@ -2502,6 +2519,61 @@ class B(A): [builtins fixtures/classmethod.pyi] [out] +[case testSubtypeOverloadCoveringMultipleSupertypeOverloadsSucceeds] +from foo import * +[file foo.pyi] +from typing import overload + + +class A: pass +class B(A): pass +class C(A): pass +class D: pass + + +class Super: + @overload + def foo(self, a: B) -> C: pass + @overload + def foo(self, a: C) -> A: pass + @overload + def foo(self, a: D) -> D: pass + +class Sub(Super): + @overload + def foo(self, a: A) -> C: pass + @overload + def foo(self, a: D) -> D: pass +[builtins fixtures/classmethod.pyi] +[out] + +[case testSubtypeOverloadWithOverlappingArgumentsButWrongReturnType] +from foo import * +[file foo.pyi] +from typing import overload + + +class A: pass +class B(A): pass +class C: pass + + +class Super: + @overload + def foo(self, a: A) -> A: pass + @overload + def foo(self, a: C) -> C: pass + +class Sub(Super): + @overload # E: Signature of "foo" incompatible with supertype "Super" + def foo(self, a: A) -> A: pass + @overload + def foo(self, a: B) -> C: pass + @overload + def foo(self, a: C) -> C: pass +[builtins fixtures/classmethod.pyi] +[out] + [case testTypeTypeOverlapsWithObjectAndType] from foo import * [file foo.pyi] From eb36c415d2608299159405e4e3a607fd2bb56801 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Tue, 7 Nov 2017 14:20:27 -0600 Subject: [PATCH 5/5] Be more lenient with detecting invalid subtype overloads --- mypy/subtypes.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0dcbf9fa07c54..3a4c7fdc0fd54 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -251,6 +251,8 @@ def visit_overloaded(self, left: Overloaded) -> bool: elif isinstance(right, Overloaded): # Ensure each overload in the right side (the supertype) is accounted for. previous_match_left_index = -1 + matched_overloads = set() + possible_invalid_overloads = set() for right_index, right_item in enumerate(right.items()): found_match = False @@ -266,18 +268,27 @@ def visit_overloaded(self, left: Overloaded) -> bool: # Update the index of the previous match. previous_match_left_index = left_index found_match = True + matched_overloads.add(left_item) + possible_invalid_overloads.discard(left_item) else: # If this one overlaps with the supertype in any way, but it wasn't - # an exact match, then it's a type error. + # an exact match, then it's a potential error. if (is_callable_subtype(left_item, right_item, ignore_return=True, ignore_pos_arg_names=self.ignore_pos_arg_names) or is_callable_subtype(right_item, left_item, ignore_return=True, ignore_pos_arg_names=self.ignore_pos_arg_names)): - return False + # If this is an overload that's already been matched, there's no + # problem. + if left_item not in matched_overloads: + possible_invalid_overloads.add(left_item) if not found_match: return False + if possible_invalid_overloads: + # There were potentially invalid overloads that were never matched to the + # supertype. + return False return True elif isinstance(right, UnboundType): return True