Skip to content

Commit d187ab3

Browse files
kirbyfan64ilevkivskyi
authored andcommitted
Allow subtypes to define more overloads than their supertype (#3263)
Fixes #3262.
1 parent 4ccde20 commit d187ab3

File tree

2 files changed

+121
-8
lines changed

2 files changed

+121
-8
lines changed

mypy/subtypes.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,46 @@ def visit_overloaded(self, left: Overloaded) -> bool:
289289
return True
290290
return False
291291
elif isinstance(right, Overloaded):
292-
# TODO: this may be too restrictive
293-
if len(left.items()) != len(right.items()):
294-
return False
295-
for i in range(len(left.items())):
296-
if not is_subtype(left.items()[i], right.items()[i], self.check_type_parameter,
297-
ignore_pos_arg_names=self.ignore_pos_arg_names):
292+
# Ensure each overload in the right side (the supertype) is accounted for.
293+
previous_match_left_index = -1
294+
matched_overloads = set()
295+
possible_invalid_overloads = set()
296+
297+
for right_index, right_item in enumerate(right.items()):
298+
found_match = False
299+
300+
for left_index, left_item in enumerate(left.items()):
301+
subtype_match = is_subtype(left_item, right_item, self.check_type_parameter,
302+
ignore_pos_arg_names=self.ignore_pos_arg_names)
303+
304+
# Order matters: we need to make sure that the index of
305+
# this item is at least the index of the previous one.
306+
if subtype_match and previous_match_left_index <= left_index:
307+
if not found_match:
308+
# Update the index of the previous match.
309+
previous_match_left_index = left_index
310+
found_match = True
311+
matched_overloads.add(left_item)
312+
possible_invalid_overloads.discard(left_item)
313+
else:
314+
# If this one overlaps with the supertype in any way, but it wasn't
315+
# an exact match, then it's a potential error.
316+
if (is_callable_subtype(left_item, right_item, ignore_return=True,
317+
ignore_pos_arg_names=self.ignore_pos_arg_names) or
318+
is_callable_subtype(right_item, left_item, ignore_return=True,
319+
ignore_pos_arg_names=self.ignore_pos_arg_names)):
320+
# If this is an overload that's already been matched, there's no
321+
# problem.
322+
if left_item not in matched_overloads:
323+
possible_invalid_overloads.add(left_item)
324+
325+
if not found_match:
298326
return False
327+
328+
if possible_invalid_overloads:
329+
# There were potentially invalid overloads that were never matched to the
330+
# supertype.
331+
return False
299332
return True
300333
elif isinstance(right, UnboundType):
301334
return True

test-data/unit/check-classes.test

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,8 +1606,6 @@ class B(A):
16061606
def __add__(self, x: str) -> A: pass
16071607
@overload
16081608
def __add__(self, x: type) -> A: pass
1609-
[out]
1610-
tmp/foo.pyi:8: error: Signature of "__add__" incompatible with supertype "A"
16111609

16121610
[case testOverloadedOperatorMethodOverrideWithSwitchedItemOrder]
16131611
from foo import *
@@ -2774,6 +2772,88 @@ reveal_type(f(BChild())) # E: Revealed type is 'foo.B'
27742772
[builtins fixtures/classmethod.pyi]
27752773
[out]
27762774

2775+
[case testSubtypeWithMoreOverloadsThanSupertypeSucceeds]
2776+
from foo import *
2777+
[file foo.pyi]
2778+
from typing import overload
2779+
2780+
2781+
class X: pass
2782+
class Y: pass
2783+
class Z: pass
2784+
2785+
2786+
class A:
2787+
@overload
2788+
def f(self, x: X) -> X: pass
2789+
@overload
2790+
def f(self, y: Y) -> Y: pass
2791+
2792+
class B(A):
2793+
@overload
2794+
def f(self, x: X) -> X: pass
2795+
@overload
2796+
def f(self, y: Y) -> Y: pass
2797+
@overload
2798+
def f(self, z: Z) -> Z: pass
2799+
[builtins fixtures/classmethod.pyi]
2800+
[out]
2801+
2802+
[case testSubtypeOverloadCoveringMultipleSupertypeOverloadsSucceeds]
2803+
from foo import *
2804+
[file foo.pyi]
2805+
from typing import overload
2806+
2807+
2808+
class A: pass
2809+
class B(A): pass
2810+
class C(A): pass
2811+
class D: pass
2812+
2813+
2814+
class Super:
2815+
@overload
2816+
def foo(self, a: B) -> C: pass
2817+
@overload
2818+
def foo(self, a: C) -> A: pass
2819+
@overload
2820+
def foo(self, a: D) -> D: pass
2821+
2822+
class Sub(Super):
2823+
@overload
2824+
def foo(self, a: A) -> C: pass
2825+
@overload
2826+
def foo(self, a: D) -> D: pass
2827+
[builtins fixtures/classmethod.pyi]
2828+
[out]
2829+
2830+
[case testSubtypeOverloadWithOverlappingArgumentsButWrongReturnType]
2831+
from foo import *
2832+
[file foo.pyi]
2833+
from typing import overload
2834+
2835+
2836+
class A: pass
2837+
class B(A): pass
2838+
class C: pass
2839+
2840+
2841+
class Super:
2842+
@overload
2843+
def foo(self, a: A) -> A: pass
2844+
@overload
2845+
def foo(self, a: C) -> C: pass
2846+
2847+
class Sub(Super):
2848+
@overload # E: Signature of "foo" incompatible with supertype "Super"
2849+
def foo(self, a: A) -> A: pass
2850+
@overload
2851+
def foo(self, a: B) -> C: pass
2852+
@overload
2853+
def foo(self, a: C) -> C: pass
2854+
[builtins fixtures/classmethod.pyi]
2855+
[out]
2856+
27772857
[case testTypeTypeOverlapsWithObjectAndType]
27782858
from foo import *
27792859
[file foo.pyi]

0 commit comments

Comments
 (0)