Skip to content

Commit 15f866c

Browse files
committed
Support multiple isinstance in a single if.
This also squashes a minor weirdness where in order to infer isinstance for resolution further in an and-expr, we were parsing for isinstance in the entire expression! Overall this is pretty self-explanatory. We get rid of the 'kind' stuff and just return a tuple of optional dicts. Fixes #900.
1 parent 410576f commit 15f866c

File tree

3 files changed

+100
-76
lines changed

3 files changed

+100
-76
lines changed

mypy/checker.py

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import itertools
44

5-
from typing import Any, Dict, Set, List, cast, Tuple, Callable, TypeVar, Union
5+
from typing import (
6+
Any, Dict, Set, List, cast, Tuple, Callable, TypeVar, Union, Optional
7+
)
68

79
from mypy.errors import Errors
810
from mypy.nodes import (
@@ -45,11 +47,6 @@
4547
from mypy.meet import meet_simple, meet_simple_away, nearest_builtin_ancestor, is_overlapping_types
4648

4749

48-
# Kinds of isinstance checks.
49-
ISINSTANCE_OVERLAPPING = 0
50-
ISINSTANCE_ALWAYS_TRUE = 1
51-
ISINSTANCE_ALWAYS_FALSE = 2
52-
5350
T = TypeVar('T')
5451

5552

@@ -1433,16 +1430,21 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
14331430
for e, b in zip(s.expr, s.body):
14341431
t = self.accept(e)
14351432
self.check_not_void(t, e)
1436-
var, type, elsetype, kind = find_isinstance_check(e, self.type_map,
1437-
self.typing_mode_weak())
1438-
if kind == ISINSTANCE_ALWAYS_FALSE:
1433+
if_map, else_map = find_isinstance_check(
1434+
e, self.type_map,
1435+
self.typing_mode_weak()
1436+
)
1437+
if if_map is None:
1438+
# The condition is always false
14391439
# XXX should issue a warning?
14401440
pass
14411441
else:
14421442
# Only type check body if the if condition can be true.
14431443
self.binder.push_frame()
1444-
if var:
1445-
self.binder.push(var, type)
1444+
if if_map:
1445+
for var, type in if_map.items():
1446+
self.binder.push(var, type)
1447+
14461448
self.accept(b)
14471449
_, frame = self.binder.pop_frame()
14481450
if not self.breaking_out:
@@ -1451,9 +1453,10 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
14511453

14521454
self.breaking_out = False
14531455

1454-
if var:
1455-
self.binder.push(var, elsetype)
1456-
if kind == ISINSTANCE_ALWAYS_TRUE:
1456+
if else_map:
1457+
for var, type in else_map.items():
1458+
self.binder.push(var, type)
1459+
if else_map is None:
14571460
# The condition is always true => remaining elif/else blocks
14581461
# can never be reached.
14591462

@@ -2046,24 +2049,17 @@ def map_type_from_supertype(typ: Type, sub_info: TypeInfo,
20462049

20472050
def find_isinstance_check(node: Node,
20482051
type_map: Dict[Node, Type],
2049-
weak: bool=False) -> Tuple[Node, Type, Type, int]:
2050-
"""Check if node is an isinstance(variable, type) check.
2051-
2052-
If successful, return tuple (variable, target-type, else-type,
2053-
kind); otherwise, return (None, AnyType, AnyType, -1).
2052+
weak: bool=False) \
2053+
-> Tuple[Optional[Dict[Node, Type]], Optional[Dict[Node, Type]]]:
2054+
"""Find any isinstance checks (within a chain of ands)".
20542055
2055-
When successful, the kind takes one of these values:
2056+
Return value is a map of variables to their types if the condition
2057+
is true and a map of variables to their types if the condition is false.
20562058
2057-
ISINSTANCE_OVERLAPPING: The type of variable and the target type are
2058-
partially overlapping => the test result can be True or False.
2059-
ISINSTANCE_ALWAYS_TRUE: The target type at least as general as the
2060-
variable type => the test is always True.
2061-
ISINSTANCE_ALWAYS_FALSE: The target type and the variable type are not
2062-
overlapping => the test is always False.
2059+
If either of the values in the tuple is None, then that particular
2060+
branch can never occur.
20632061
2064-
If it is an isinstance check, but we don't understand the argument
2065-
type, then in weak mode it is treated as Any and in non-weak mode
2066-
it is not treated as an isinstance.
2062+
Guaranteed to not return None, None. (But may return {}, {})
20672063
"""
20682064
if isinstance(node, CallExpr):
20692065
if refers_to_fullname(node.callee, 'builtins.isinstance'):
@@ -2072,46 +2068,45 @@ def find_isinstance_check(node: Node,
20722068
vartype = type_map[expr]
20732069
type = get_isinstance_type(node.args[1], type_map)
20742070
if type:
2075-
kind = ISINSTANCE_OVERLAPPING
20762071
elsetype = vartype
20772072
if vartype:
20782073
if is_proper_subtype(vartype, type):
2079-
kind = ISINSTANCE_ALWAYS_TRUE
20802074
elsetype = None
2075+
return {expr: type}, None
20812076
elif not is_overlapping_types(vartype, type):
2082-
kind = ISINSTANCE_ALWAYS_FALSE
2077+
return None, {expr: elsetype}
20832078
else:
20842079
elsetype = restrict_subtype_away(vartype, type)
2085-
return expr, type, elsetype, kind
2080+
return {expr: type}, {expr: elsetype}
20862081
else:
20872082
# An isinstance check, but we don't understand the type
20882083
if weak:
2089-
return expr, AnyType(), vartype, ISINSTANCE_OVERLAPPING
2084+
return {expr: AnyType()}, {expr: vartype}
20902085
elif isinstance(node, OpExpr) and node.op == 'and':
2091-
# XXX We should extend this to support two isinstance checks in the same
2092-
# expression
2093-
(var, type, elsetype, kind) = find_isinstance_check(node.left, type_map, weak)
2094-
if var is None:
2095-
(var, type, elsetype, kind) = find_isinstance_check(node.left, type_map, weak)
2096-
if var:
2097-
if kind == ISINSTANCE_ALWAYS_TRUE:
2098-
kind = ISINSTANCE_OVERLAPPING
2099-
return (var, type, AnyType(), kind)
2086+
left_if_vars, right_else_vars = find_isinstance_check(
2087+
node.left,
2088+
type_map,
2089+
weak,
2090+
)
2091+
2092+
right_if_vars, right_else_vars = find_isinstance_check(
2093+
node.right,
2094+
type_map,
2095+
weak,
2096+
)
2097+
if left_if_vars:
2098+
left_if_vars.update(right_if_vars)
2099+
else:
2100+
left_if_vars = right_if_vars
2101+
2102+
# Make no claim about the types in else
2103+
return left_if_vars, {}
21002104
elif isinstance(node, UnaryExpr) and node.op == 'not':
2101-
(var, type, elsetype, kind) = find_isinstance_check(node.expr, type_map, weak)
2102-
return (var, elsetype, type, invert_isinstance_kind(kind))
2105+
left, right = find_isinstance_check(node.expr, type_map, weak)
2106+
return right, left
21032107

21042108
# Not a supported isinstance check
2105-
return None, AnyType(), AnyType(), -1
2106-
2107-
2108-
def invert_isinstance_kind(kind: int) -> int:
2109-
if kind == ISINSTANCE_ALWAYS_TRUE:
2110-
return ISINSTANCE_ALWAYS_FALSE
2111-
elif kind == ISINSTANCE_ALWAYS_FALSE:
2112-
return ISINSTANCE_ALWAYS_TRUE
2113-
else:
2114-
return kind
2109+
return {}, {}
21152110

21162111

21172112
def get_isinstance_type(node: Node, type_map: Dict[Node, Type]) -> Type:

mypy/checkexpr.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -911,17 +911,21 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
911911
left_type = self.accept(e.left, ctx)
912912

913913
if e.op == 'and':
914-
var, type, elsetype, kind = \
915-
mypy.checker.find_isinstance_check(e, self.chk.type_map,
914+
# else_map unused
915+
if_map, else_map = \
916+
mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
916917
self.chk.typing_mode_weak())
917918
else:
918-
var = None
919-
if var:
920-
self.chk.binder.push_frame()
921-
self.chk.binder.push(var, type)
919+
if_map = None
920+
921+
self.chk.binder.push_frame()
922+
if if_map:
923+
for var, type in if_map.items():
924+
self.chk.binder.push(var, type)
925+
922926
right_type = self.accept(e.right, left_type)
923-
if var:
924-
self.chk.binder.pop_frame()
927+
928+
self.chk.binder.pop_frame()
925929

926930
self.check_not_void(left_type, context)
927931
self.check_not_void(right_type, context)
@@ -1278,22 +1282,30 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
12781282

12791283
# Gain type information from isinstance if it is there
12801284
# but only for the current expression
1281-
variable, inst_if_type, inst_else_type, kind = mypy.checker.find_isinstance_check(
1285+
if_map, else_map = mypy.checker.find_isinstance_check(
12821286
e.cond,
12831287
self.chk.type_map,
12841288
self.chk.typing_mode_weak())
1285-
if variable:
1286-
self.chk.binder.push_frame()
1287-
self.chk.binder.push(variable, inst_if_type)
1288-
if_type = self.accept(e.if_expr)
1289-
self.chk.binder.pop_frame()
1290-
self.chk.binder.push_frame()
1291-
self.chk.binder.push(variable, inst_else_type)
1292-
else_type = self.accept(e.else_expr, context=if_type)
1293-
self.chk.binder.pop_frame()
1294-
else:
1295-
if_type = self.accept(e.if_expr)
1296-
else_type = self.accept(e.else_expr, context=if_type)
1289+
1290+
self.chk.binder.push_frame()
1291+
1292+
if if_map:
1293+
for var, type in if_map.items():
1294+
self.chk.binder.push(var, type)
1295+
1296+
if_type = self.accept(e.if_expr)
1297+
1298+
self.chk.binder.pop_frame()
1299+
self.chk.binder.push_frame()
1300+
1301+
if else_map:
1302+
for var, type in else_map.items():
1303+
self.chk.binder.push(var, type)
1304+
1305+
else_type = self.accept(e.else_expr, context=if_type)
1306+
1307+
self.chk.binder.pop_frame()
1308+
12971309
return join.join_types(if_type, else_type)
12981310

12991311
#

mypy/test/data/check-isinstance.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,3 +721,20 @@ x.flag if isinstance(x, B) else 0
721721
0 if not isinstance(x, B) else x.flag
722722
0 if isinstance(x, B) else x.flag # E: "A" has no attribute "flag"
723723
[builtins fixtures/isinstancelist.py]
724+
[case testIsinstanceMultiAnd]
725+
class A:
726+
pass
727+
728+
class B(A):
729+
flag = 1
730+
731+
class C(A):
732+
glaf = 1
733+
734+
x = B() # type: A
735+
y = C() # type: A
736+
737+
if isinstance(x, B) and isinstance(y, C):
738+
x.flag += 1
739+
y.glaf += 1
740+
[builtins fixtures/isinstancelist.py]

0 commit comments

Comments
 (0)