Skip to content

Support multiple isinstance in a single if. #905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 50 additions & 55 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import itertools

from typing import Any, Dict, Set, List, cast, Tuple, Callable, TypeVar, Union
from typing import (
Any, Dict, Set, List, cast, Tuple, Callable, TypeVar, Union, Optional
)

from mypy.errors import Errors
from mypy.nodes import (
Expand Down Expand Up @@ -45,11 +47,6 @@
from mypy.meet import meet_simple, meet_simple_away, nearest_builtin_ancestor, is_overlapping_types


# Kinds of isinstance checks.
ISINSTANCE_OVERLAPPING = 0
ISINSTANCE_ALWAYS_TRUE = 1
ISINSTANCE_ALWAYS_FALSE = 2

T = TypeVar('T')


Expand Down Expand Up @@ -1433,16 +1430,21 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
for e, b in zip(s.expr, s.body):
t = self.accept(e)
self.check_not_void(t, e)
var, type, elsetype, kind = find_isinstance_check(e, self.type_map,
self.typing_mode_weak())
if kind == ISINSTANCE_ALWAYS_FALSE:
if_map, else_map = find_isinstance_check(
e, self.type_map,
self.typing_mode_weak()
)
if if_map is None:
# The condition is always false
# XXX should issue a warning?
pass
else:
# Only type check body if the if condition can be true.
self.binder.push_frame()
if var:
self.binder.push(var, type)
if if_map:
for var, type in if_map.items():
self.binder.push(var, type)

self.accept(b)
_, frame = self.binder.pop_frame()
if not self.breaking_out:
Expand All @@ -1451,9 +1453,10 @@ def visit_if_stmt(self, s: IfStmt) -> Type:

self.breaking_out = False

if var:
self.binder.push(var, elsetype)
if kind == ISINSTANCE_ALWAYS_TRUE:
if else_map:
for var, type in else_map.items():
self.binder.push(var, type)
if else_map is None:
# The condition is always true => remaining elif/else blocks
# can never be reached.

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

def find_isinstance_check(node: Node,
type_map: Dict[Node, Type],
weak: bool=False) -> Tuple[Node, Type, Type, int]:
"""Check if node is an isinstance(variable, type) check.

If successful, return tuple (variable, target-type, else-type,
kind); otherwise, return (None, AnyType, AnyType, -1).
weak: bool=False) \
-> Tuple[Optional[Dict[Node, Type]], Optional[Dict[Node, Type]]]:
"""Find any isinstance checks (within a chain of ands)".

When successful, the kind takes one of these values:
Return value is a map of variables to their types if the condition
is true and a map of variables to their types if the condition is false.

ISINSTANCE_OVERLAPPING: The type of variable and the target type are
partially overlapping => the test result can be True or False.
ISINSTANCE_ALWAYS_TRUE: The target type at least as general as the
variable type => the test is always True.
ISINSTANCE_ALWAYS_FALSE: The target type and the variable type are not
overlapping => the test is always False.
If either of the values in the tuple is None, then that particular
branch can never occur.

If it is an isinstance check, but we don't understand the argument
type, then in weak mode it is treated as Any and in non-weak mode
it is not treated as an isinstance.
Guaranteed to not return None, None. (But may return {}, {})
"""
if isinstance(node, CallExpr):
if refers_to_fullname(node.callee, 'builtins.isinstance'):
Expand All @@ -2072,46 +2068,45 @@ def find_isinstance_check(node: Node,
vartype = type_map[expr]
type = get_isinstance_type(node.args[1], type_map)
if type:
kind = ISINSTANCE_OVERLAPPING
elsetype = vartype
if vartype:
if is_proper_subtype(vartype, type):
kind = ISINSTANCE_ALWAYS_TRUE
elsetype = None
return {expr: type}, None
elif not is_overlapping_types(vartype, type):
kind = ISINSTANCE_ALWAYS_FALSE
return None, {expr: elsetype}
else:
elsetype = restrict_subtype_away(vartype, type)
return expr, type, elsetype, kind
return {expr: type}, {expr: elsetype}
else:
# An isinstance check, but we don't understand the type
if weak:
return expr, AnyType(), vartype, ISINSTANCE_OVERLAPPING
return {expr: AnyType()}, {expr: vartype}
elif isinstance(node, OpExpr) and node.op == 'and':
# XXX We should extend this to support two isinstance checks in the same
# expression
(var, type, elsetype, kind) = find_isinstance_check(node.left, type_map, weak)
if var is None:
(var, type, elsetype, kind) = find_isinstance_check(node.left, type_map, weak)
if var:
if kind == ISINSTANCE_ALWAYS_TRUE:
kind = ISINSTANCE_OVERLAPPING
return (var, type, AnyType(), kind)
left_if_vars, right_else_vars = find_isinstance_check(
node.left,
type_map,
weak,
)

right_if_vars, right_else_vars = find_isinstance_check(
node.right,
type_map,
weak,
)
if left_if_vars:
left_if_vars.update(right_if_vars)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if right_if_vars is None?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, how come this didn't get caught by mypy running on itself?

else:
left_if_vars = right_if_vars

# Make no claim about the types in else
return left_if_vars, {}
elif isinstance(node, UnaryExpr) and node.op == 'not':
(var, type, elsetype, kind) = find_isinstance_check(node.expr, type_map, weak)
return (var, elsetype, type, invert_isinstance_kind(kind))
left, right = find_isinstance_check(node.expr, type_map, weak)
return right, left

# Not a supported isinstance check
return None, AnyType(), AnyType(), -1


def invert_isinstance_kind(kind: int) -> int:
if kind == ISINSTANCE_ALWAYS_TRUE:
return ISINSTANCE_ALWAYS_FALSE
elif kind == ISINSTANCE_ALWAYS_FALSE:
return ISINSTANCE_ALWAYS_TRUE
else:
return kind
return {}, {}


def get_isinstance_type(node: Node, type_map: Dict[Node, Type]) -> Type:
Expand Down
54 changes: 33 additions & 21 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,17 +911,21 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
left_type = self.accept(e.left, ctx)

if e.op == 'and':
var, type, elsetype, kind = \
mypy.checker.find_isinstance_check(e, self.chk.type_map,
# else_map unused
if_map, else_map = \
mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
self.chk.typing_mode_weak())
else:
var = None
if var:
self.chk.binder.push_frame()
self.chk.binder.push(var, type)
if_map = None

self.chk.binder.push_frame()
if if_map:
for var, type in if_map.items():
self.chk.binder.push(var, type)

right_type = self.accept(e.right, left_type)
if var:
self.chk.binder.pop_frame()

self.chk.binder.pop_frame()

self.check_not_void(left_type, context)
self.check_not_void(right_type, context)
Expand Down Expand Up @@ -1278,22 +1282,30 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:

# Gain type information from isinstance if it is there
# but only for the current expression
variable, inst_if_type, inst_else_type, kind = mypy.checker.find_isinstance_check(
if_map, else_map = mypy.checker.find_isinstance_check(
e.cond,
self.chk.type_map,
self.chk.typing_mode_weak())
if variable:
self.chk.binder.push_frame()
self.chk.binder.push(variable, inst_if_type)
if_type = self.accept(e.if_expr)
self.chk.binder.pop_frame()
self.chk.binder.push_frame()
self.chk.binder.push(variable, inst_else_type)
else_type = self.accept(e.else_expr, context=if_type)
self.chk.binder.pop_frame()
else:
if_type = self.accept(e.if_expr)
else_type = self.accept(e.else_expr, context=if_type)

self.chk.binder.push_frame()

if if_map:
for var, type in if_map.items():
self.chk.binder.push(var, type)

if_type = self.accept(e.if_expr)

self.chk.binder.pop_frame()
self.chk.binder.push_frame()

if else_map:
for var, type in else_map.items():
self.chk.binder.push(var, type)

else_type = self.accept(e.else_expr, context=if_type)

self.chk.binder.pop_frame()

return join.join_types(if_type, else_type)

#
Expand Down
17 changes: 17 additions & 0 deletions mypy/test/data/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,20 @@ x.flag if isinstance(x, B) else 0
0 if not isinstance(x, B) else x.flag
0 if isinstance(x, B) else x.flag # E: "A" has no attribute "flag"
[builtins fixtures/isinstancelist.py]
[case testIsinstanceMultiAnd]
class A:
pass

class B(A):
flag = 1

class C(A):
glaf = 1

x = B() # type: A
y = C() # type: A

if isinstance(x, B) and isinstance(y, C):
x.flag += 1
y.glaf += 1
[builtins fixtures/isinstancelist.py]