Skip to content

Commit 347a168

Browse files
committed
Avoid false "Incompatible return value type" errors when dealing with isinstance, constrained type variables and multiple inheritance (fixes python#13800).
The idea is to make a union of the current expanded return type and the previously expanded return types if the latter are among the bases of intersections generated by `isinstance` checks.
1 parent 2413578 commit 347a168

File tree

2 files changed

+106
-7
lines changed

2 files changed

+106
-7
lines changed

mypy/checker.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
313313
scope: CheckerScope
314314
# Stack of function return types
315315
return_types: list[Type]
316+
# Registry of the return types already evaluated when checking a function
317+
# definition that includes constrained type variables
318+
previously_expanded_return_types: list[Type]
316319
# Flags; true for dynamically typed functions
317320
dynamic_funcs: list[bool]
318321
# Stack of collections of variables with partial types
@@ -383,6 +386,7 @@ def __init__(
383386
self.binder = ConditionalTypeBinder()
384387
self.globals = tree.names
385388
self.return_types = []
389+
self.previously_expanded_return_types = []
386390
self.dynamic_funcs = []
387391
self.partial_types = []
388392
self.partial_reported = set()
@@ -1302,9 +1306,10 @@ def check_func_def(
13021306
):
13031307
self.note(message_registry.EMPTY_BODY_ABSTRACT, defn)
13041308

1305-
self.return_types.pop()
1309+
self.previously_expanded_return_types.append(self.return_types.pop())
13061310

13071311
self.binder = old_binder
1312+
self.previously_expanded_return_types.clear()
13081313

13091314
def check_unbound_return_typevar(self, typ: CallableType) -> None:
13101315
"""Fails when the return typevar is not defined in arguments."""
@@ -4225,7 +4230,28 @@ def visit_if_stmt(self, s: IfStmt) -> None:
42254230
# XXX Issue a warning if condition is always False?
42264231
with self.binder.frame_context(can_skip=True, fall_through=2):
42274232
self.push_type_map(if_map)
4228-
self.accept(b)
4233+
# Eventually, broaden the expanded return type to avoid false
4234+
# "incompatible return value type" error messages (see issue 13800).
4235+
if self.return_types and (if_map is not None):
4236+
original_return_type = self.return_types[-1]
4237+
for typ in if_map.values():
4238+
typ = get_proper_type(typ)
4239+
if isinstance(typ, UnionType):
4240+
typs = flatten_nested_unions(typ.items)
4241+
else:
4242+
typs = [typ]
4243+
for typ in typs:
4244+
typ = get_proper_type(typ)
4245+
if isinstance(typ, Instance) and (typ.type.is_intersection):
4246+
for base in typ.type.bases:
4247+
if base in self.previously_expanded_return_types:
4248+
self.return_types[-1] = UnionType.make_union(
4249+
(self.return_types[-1], base)
4250+
)
4251+
self.accept(b)
4252+
self.return_types[-1] = original_return_type
4253+
else:
4254+
self.accept(b)
42294255

42304256
# XXX Issue a warning if condition is always True?
42314257
self.push_type_map(else_map)

test-data/unit/check-isinstance.test

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,24 +2486,97 @@ class B:
24862486
attr: int
24872487
class C:
24882488
attr: str
2489+
class D:
2490+
attr: int
2491+
2492+
# basic test cases:
24892493

24902494
T1 = TypeVar('T1', A, B)
24912495
def f1(x: T1) -> T1:
24922496
if isinstance(x, A):
2493-
# The error message is confusing, but we indeed do run into problems if
2494-
# 'x' is a subclass of A and B
2495-
return A() # E: Incompatible return value type (got "A", expected "B")
2497+
# No error, because "A" takes priority over "B" when passing a subclass
2498+
# of "A" and "B" to "f1".
2499+
return A()
24962500
else:
24972501
return B()
24982502

24992503
T2 = TypeVar('T2', B, C)
25002504
def f2(x: T2) -> T2:
25012505
if isinstance(x, B):
2502-
# In contrast, it's impossible for a subclass of "B" and "C" to
2503-
# exist, so this is fine
2506+
# No error, because a subclass of "B" and "C" cannot exist.
25042507
return B()
25052508
else:
25062509
return C()
2510+
2511+
T3 = TypeVar('T3', B, A)
2512+
def f3(x: T3) -> T3:
2513+
if isinstance(x, A):
2514+
# The error message is confusing, but we indeed do run into problems if
2515+
# 'x' is a subclass of "A" and "B" and "B" takes priority over "A".
2516+
return A() # E: Incompatible return value type (got "A", expected "B")
2517+
else:
2518+
return B()
2519+
2520+
# more complex test cases:
2521+
2522+
T4 = TypeVar('T4', A, B, D)
2523+
def f4(x: T4) -> T4:
2524+
if isinstance(x, A):
2525+
return A()
2526+
elif isinstance(x, B):
2527+
return B()
2528+
else:
2529+
return D()
2530+
2531+
T5 = TypeVar('T5', A, B, D)
2532+
def f5(x: T5) -> T5:
2533+
if isinstance(x, A):
2534+
return A()
2535+
if isinstance(x, B):
2536+
return B()
2537+
return D()
2538+
2539+
T6 = TypeVar('T6', A, B, D)
2540+
def f6(x: T6) -> T6:
2541+
if isinstance(x, A):
2542+
if isinstance(x, B):
2543+
return B() # E: Incompatible return value type (got "B", expected "A")
2544+
return A()
2545+
if isinstance(x, B):
2546+
return B()
2547+
return D()
2548+
2549+
T7 = TypeVar('T7', A, B, D)
2550+
def f7(x: T7) -> T7:
2551+
if isinstance(x, (A, B)):
2552+
return A() # E: Incompatible return value type (got "A", expected "B")
2553+
return D()
2554+
2555+
T8 = TypeVar('T8', A, B, D)
2556+
def f8(x: T8) -> T8:
2557+
if isinstance(x, (A, B)):
2558+
if isinstance(x, A):
2559+
return A()
2560+
return B()
2561+
return D()
2562+
2563+
T9 = TypeVar('T9', A, D, B)
2564+
def f9(x: T9) -> T9:
2565+
if isinstance(x, A):
2566+
return A()
2567+
elif isinstance(x, B):
2568+
return B() # E: Incompatible return value type (got "B", expected "D")
2569+
else:
2570+
return D()
2571+
2572+
T10 = TypeVar('T10', A, D, B)
2573+
def f10(x: T10) -> T10:
2574+
if isinstance(x, (A, B)):
2575+
if isinstance(x, A):
2576+
return A()
2577+
return B() # E: Incompatible return value type (got "B", expected "Union[D, A]")
2578+
return D()
2579+
25072580
[builtins fixtures/isinstance.pyi]
25082581

25092582
[case testIsInstanceAdHocIntersectionUsage]

0 commit comments

Comments
 (0)