Skip to content
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

Pr/strict optional #1562

Merged
merged 31 commits into from
Jun 10, 2016
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3dcb752
Basic strict Optional checking
ddfisher May 4, 2016
30f3a0f
WIP
ddfisher May 12, 2016
b5e8433
cleanup
ddfisher May 19, 2016
8f8fb49
the return of uninhabitedtype
ddfisher May 19, 2016
8c0577a
fix tests WIP
ddfisher May 19, 2016
b136ef5
tests, bug fixes, partial types
ddfisher May 20, 2016
e274c13
isinstance checks
ddfisher May 20, 2016
6047860
fixup todos
ddfisher May 20, 2016
d60c93f
allow None class variable declarations
ddfisher May 20, 2016
48f9311
Merge branch 'master' into PR/strict-optional
ddfisher May 20, 2016
07cce60
Rename experimental -> experiments
ddfisher May 20, 2016
15cad44
add missing type annotations
ddfisher May 20, 2016
53ebea0
fix lint errors
ddfisher May 21, 2016
5625c75
rename UnboundType.ret_type -> is_ret_type
ddfisher May 23, 2016
157f57d
fix lack of type narrowing for member variables
ddfisher May 23, 2016
0a2c4e1
gate new isinstance checks behind experiments flag
ddfisher May 24, 2016
b6f6032
fix tests broken by member type variable narrowing
ddfisher May 24, 2016
35a6b2e
strengthen tests per Jukka's suggestions
ddfisher May 24, 2016
8b49b8f
Fixup type lattice per Reid's comments
ddfisher May 26, 2016
d9996da
Address Reid's remaining comments
ddfisher May 26, 2016
ea46191
Generalize some NoneTyp isinstance checks found by Reid
ddfisher May 26, 2016
161a2bc
Fix other crash as per Reid
ddfisher May 27, 2016
2149c11
Merge branch 'master' into PR/strict-optional
ddfisher May 27, 2016
ffd4301
Merge branch 'master' into PR/strict-optional
ddfisher Jun 6, 2016
a40636e
unnest functions
ddfisher Jun 6, 2016
6661f93
Fixup tests; add new test
ddfisher Jun 6, 2016
fbbab64
update docstrings
ddfisher Jun 6, 2016
b9e309f
fix lint errors
ddfisher Jun 6, 2016
87f46a0
add test for overloads and fix overloading
ddfisher Jun 7, 2016
7247430
minor test fixup
ddfisher Jun 10, 2016
ba743ec
Merge branch 'master' into PR/strict-optional
ddfisher Jun 10, 2016
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
114 changes: 82 additions & 32 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from mypy.types import (
Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType,
Instance, NoneTyp, ErrorType, strip_type,
UnionType, TypeVarType, PartialType, DeletedType
UnionType, TypeVarType, PartialType, DeletedType, UninhabitedType
)
from mypy.sametypes import is_same_type
from mypy.messages import MessageBuilder
Expand All @@ -51,6 +51,8 @@
from mypy.treetransform import TransformVisitor
from mypy.meet import meet_simple, nearest_builtin_ancestor, is_overlapping_types

from mypy import experimental


T = TypeVar('T')

Expand Down Expand Up @@ -1171,7 +1173,10 @@ def check_assignment(self, lvalue: Node, rvalue: Node, infer_lvalue_type: bool =
partial_types = self.find_partial_types(var)
if partial_types is not None:
if not self.current_node_deferred:
var.type = rvalue_type
if experimental.STRICT_OPTIONAL:
var.type = UnionType.make_simplified_union([rvalue_type, NoneTyp()])
else:
var.type = rvalue_type
else:
var.type = None
del partial_types[var]
Expand Down Expand Up @@ -1438,16 +1443,16 @@ def infer_variable_type(self, name: Var, lvalue: Node,
self.set_inferred_type(name, lvalue, init_type)

def infer_partial_type(self, name: Var, lvalue: Node, init_type: Type) -> bool:
if isinstance(init_type, NoneTyp):
partial_type = PartialType(None, name)
if isinstance(init_type, (NoneTyp, UninhabitedType)):
partial_type = PartialType(None, name, [init_type])
elif isinstance(init_type, Instance):
fullname = init_type.type.fullname()
if ((fullname == 'builtins.list' or fullname == 'builtins.set' or
fullname == 'builtins.dict')
and isinstance(init_type.args[0], NoneTyp)
and (fullname != 'builtins.dict' or isinstance(init_type.args[1], NoneTyp))
and isinstance(lvalue, NameExpr)):
partial_type = PartialType(init_type.type, name)
if (isinstance(lvalue, NameExpr) and
(fullname == 'builtins.list' or
fullname == 'builtins.set' or
fullname == 'builtins.dict') and
all(isinstance(t, (NoneTyp, UninhabitedType)) for t in init_type.args)):
partial_type = PartialType(init_type.type, name, init_type.args)
else:
return False
else:
Expand Down Expand Up @@ -1530,8 +1535,8 @@ def try_infer_partial_type_from_indexed_assignment(
self, lvalue: IndexExpr, rvalue: Node) -> None:
# TODO: Should we share some of this with try_infer_partial_type?
if isinstance(lvalue.base, RefExpr) and isinstance(lvalue.base.node, Var):
var = cast(Var, lvalue.base.node)
if var is not None and isinstance(var.type, PartialType):
var = lvalue.base.node
if isinstance(var.type, PartialType):
type_type = var.type.type
if type_type is None:
return # The partial type is None.
Expand All @@ -1543,10 +1548,12 @@ def try_infer_partial_type_from_indexed_assignment(
# TODO: Don't infer things twice.
key_type = self.accept(lvalue.index)
value_type = self.accept(rvalue)
if is_valid_inferred_type(key_type) and is_valid_inferred_type(value_type):
full_key_type = UnionType.make_simplified_union([key_type, var.type.inner_types[0]])
full_value_type = UnionType.make_simplified_union([value_type, var.type.inner_types[0]])
if is_valid_inferred_type(full_key_type) and is_valid_inferred_type(full_value_type):
if not self.current_node_deferred:
var.type = self.named_generic_type('builtins.dict',
[key_type, value_type])
[full_key_type, full_value_type])
del partial_types[var]

def visit_expression_stmt(self, s: ExpressionStmt) -> Type:
Expand Down Expand Up @@ -1856,7 +1863,10 @@ def analyze_iterable_item_type(self, expr: Node) -> Type:

self.check_not_void(iterable, expr)
if isinstance(iterable, TupleType):
joined = NoneTyp() # type: Type
if experimental.STRICT_OPTIONAL:
joined = UninhabitedType() # type: Type
else:
joined = NoneTyp()
for item in iterable.items:
joined = join_types(joined, item)
if isinstance(joined, ErrorType):
Expand Down Expand Up @@ -2282,8 +2292,12 @@ def leave_partial_types(self) -> None:
partial_types = self.partial_types.pop()
if not self.current_node_deferred:
for var, context in partial_types.items():
self.msg.fail(messages.NEED_ANNOTATION_FOR_VAR, context)
var.type = AnyType()
if experimental.STRICT_OPTIONAL and cast(PartialType, var.type).type is None:
# None partial type: assume variable is intended to have type None
var.type = NoneTyp()
else:
self.msg.fail(messages.NEED_ANNOTATION_FOR_VAR, context)
var.type = AnyType()

def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]:
for partial_types in reversed(self.partial_types):
Expand Down Expand Up @@ -2331,7 +2345,8 @@ def find_isinstance_check(node: Node,
type_map: Dict[Node, Type],
weak: bool=False) \
-> Tuple[Optional[Dict[Node, Type]], Optional[Dict[Node, Type]]]:
"""Find any isinstance checks (within a chain of ands).
"""Find any isinstance checks (within a chain of ands). Includes
implicit and explicit checks for None.

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.
Expand All @@ -2341,26 +2356,59 @@ def find_isinstance_check(node: Node,

Guaranteed to not return None, None. (But may return {}, {})
"""
def split_types(current_type: Optional[Type], type_if_true: Optional[Type], expr: Node) \
-> Tuple[Optional[Dict[Node, Type]], Optional[Dict[Node, Type]]]:
if type_if_true:
Copy link
Contributor

Choose a reason for hiding this comment

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

This function probably deserves its own docstring (it was easier to follow what was going on from context in the old version).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. Tried to give it a bit of a better name too.

if current_type:
if is_proper_subtype(current_type, type_if_true):
return {expr: type_if_true}, None
elif not is_overlapping_types(current_type, type_if_true):
return None, {expr: current_type}
else:
type_if_false = restrict_subtype_away(current_type, type_if_true)
return {expr: type_if_true}, {expr: type_if_false}
else:
return {expr: type_if_true}, {}
else:
# An isinstance check, but we don't understand the type
if weak:
return {expr: AnyType()}, {expr: vartype}
else:
return {}, {}

def is_none(n: Node) -> bool:
return isinstance(n, NameExpr) and n.fullname == 'builtins.None'

if isinstance(node, CallExpr):
if refers_to_fullname(node.callee, 'builtins.isinstance'):
expr = node.args[0]
if expr.literal == LITERAL_TYPE:
vartype = type_map[expr]
type = get_isinstance_type(node.args[1], type_map)
if type:
elsetype = vartype
if vartype:
if is_proper_subtype(vartype, type):
return {expr: type}, None
elif not is_overlapping_types(vartype, type):
return None, {expr: elsetype}
else:
elsetype = restrict_subtype_away(vartype, type)
return {expr: type}, {expr: elsetype}
else:
# An isinstance check, but we don't understand the type
if weak:
return {expr: AnyType()}, {expr: vartype}
return split_types(vartype, type, expr)
elif (isinstance(node, ComparisonExpr) and any(is_none(n) for n in node.operands)):
# Check for `x is None` and `x is not None`.
is_not = node.operators == ['is not']
if is_not or node.operators == ['is']:
if_vars = {}
else_vars = {}
for expr in node.operands:
if expr.literal == LITERAL_TYPE and not is_none(expr):
# This should only be true at most once: there should be
# two elements in node.operands, and at least one of them
# should represent a None.
vartype = type_map[expr]
Copy link
Contributor

Choose a reason for hiding this comment

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

The other crash is here; I suppose you can just do nothing if type_map[expr] is not present.

if_vars, else_vars = split_types(vartype, NoneTyp(), expr)
break

if is_not:
if_vars, else_vars = else_vars, if_vars
return if_vars, else_vars
elif (isinstance(node, RefExpr)):
# The type could be falsy, so we can't deduce anything new about the else branch
vartype = type_map[node]
_, if_vars = split_types(vartype, NoneTyp(), node)
return if_vars, {}
elif isinstance(node, OpExpr) and node.op == 'and':
left_if_vars, right_else_vars = find_isinstance_check(
node.left,
Expand Down Expand Up @@ -2543,6 +2591,8 @@ def is_valid_inferred_type(typ: Type) -> bool:
"""
if is_same_type(typ, NoneTyp()):
return False
Copy link
Contributor

Choose a reason for hiding this comment

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

Might add a comment here about how with strict Optional checking we'll infer NoneTyp later (in leave_partial_types I think) if we can't infer a specific Optional type by then, but we don't want to commit to NoneTyp now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed -- added!

if is_same_type(typ, UninhabitedType()):
return False
elif isinstance(typ, Instance):
for arg in typ.args:
if not is_valid_inferred_type(arg):
Expand Down
17 changes: 10 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,20 @@ def try_infer_partial_type(self, e: CallExpr) -> None:
var = cast(Var, e.callee.expr.node)
partial_types = self.chk.find_partial_types(var)
if partial_types is not None and not self.chk.current_node_deferred:
partial_type_type = cast(PartialType, var.type).type
if partial_type_type is None:
partial_type = cast(PartialType, var.type)
if partial_type.type is None:
# A partial None type -> can't infer anything.
return
typename = partial_type_type.fullname()
typename = partial_type.type.fullname()
methodname = e.callee.name
# Sometimes we can infer a full type for a partial List, Dict or Set type.
# TODO: Don't infer argument expression twice.
if (typename in self.item_args and methodname in self.item_args[typename]
and e.arg_kinds == [ARG_POS]):
item_type = self.accept(e.args[0])
if mypy.checker.is_valid_inferred_type(item_type):
var.type = self.chk.named_generic_type(typename, [item_type])
full_item_type = UnionType.make_simplified_union([item_type, partial_type.inner_types[0]])
if mypy.checker.is_valid_inferred_type(full_item_type):
var.type = self.chk.named_generic_type(typename, [full_item_type])
del partial_types[var]
elif (typename in self.container_args
and methodname in self.container_args[typename]
Expand All @@ -175,10 +176,12 @@ def try_infer_partial_type(self, e: CallExpr) -> None:
if isinstance(arg_type, Instance):
arg_typename = arg_type.type.fullname()
if arg_typename in self.container_args[typename][methodname]:
full_item_types = [UnionType.make_simplified_union([item_type, prev_type])
for item_type, prev_type in zip(arg_type.args, partial_type.inner_types)]
if all(mypy.checker.is_valid_inferred_type(item_type)
for item_type in arg_type.args):
for item_type in full_item_types):
var.type = self.chk.named_generic_type(typename,
list(arg_type.args))
list(full_item_types))
del partial_types[var]

def check_call_expr_with_callee_type(self, callee_type: Type,
Expand Down
5 changes: 4 additions & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from mypy.types import (
CallableType, Type, TypeVisitor, UnboundType, AnyType, Void, NoneTyp, TypeVarType,
Instance, TupleType, UnionType, Overloaded, ErasedType, PartialType, DeletedType,
is_named_instance
is_named_instance, UninhabitedType
)
from mypy.maptype import map_instance_to_supertype
from mypy import nodes
Expand Down Expand Up @@ -222,6 +222,9 @@ def visit_void(self, template: Void) -> List[Constraint]:
def visit_none_type(self, template: NoneTyp) -> List[Constraint]:
return []

def visit_uninhabited_type(self, template: UninhabitedType) -> List[Constraint]:
return []

def visit_erased_type(self, template: ErasedType) -> List[Constraint]:
return []

Expand Down
5 changes: 4 additions & 1 deletion mypy/erasetype.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from mypy.types import (
Type, TypeVisitor, UnboundType, ErrorType, AnyType, Void, NoneTyp,
Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType,
PartialType, DeletedType, TypeTranslator, TypeList
PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType
)


Expand Down Expand Up @@ -40,6 +40,9 @@ def visit_void(self, t: Void) -> Type:
def visit_none_type(self, t: NoneTyp) -> Type:
return t

def visit_uninhabited_type(self, t: UninhabitedType) -> Type:
return t

def visit_erased_type(self, t: ErasedType) -> Type:
# Should not get here.
raise RuntimeError()
Expand Down
5 changes: 4 additions & 1 deletion mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from mypy.types import (
Type, Instance, CallableType, TypeVisitor, UnboundType, ErrorType, AnyType,
Void, NoneTyp, TypeVarType, Overloaded, TupleType, UnionType, ErasedType, TypeList,
PartialType, DeletedType
PartialType, DeletedType, UninhabitedType
)


Expand Down Expand Up @@ -53,6 +53,9 @@ def visit_void(self, t: Void) -> Type:
def visit_none_type(self, t: NoneTyp) -> Type:
return t

def visit_uninhabited_type(self, t: UninhabitedType) -> Type:
return t

def visit_deleted_type(self, t: DeletedType) -> Type:
return t

Expand Down
1 change: 1 addition & 0 deletions mypy/experimental.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
STRICT_OPTIONAL = False
Copy link
Member

Choose a reason for hiding this comment

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

I don't think an adjective make a good module name. Can you make it a noun, e.g. 'experiments'?

6 changes: 5 additions & 1 deletion mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
TypeVarExpr, ClassDef,
LDEF, MDEF, GDEF, MODULE_REF)
from mypy.types import (CallableType, EllipsisType, Instance, Overloaded, TupleType,
TypeList, TypeVarType, UnboundType, UnionType, TypeVisitor)
TypeList, TypeVarType, UnboundType, UnionType, TypeVisitor,
UninhabitedType)
from mypy.visitor import NodeVisitor


Expand Down Expand Up @@ -180,6 +181,9 @@ def visit_deleted_type(self, o: Any) -> None:
def visit_none_type(self, o: Any) -> None:
pass # Nothing to descend into.

def visit_uninhabited_type(self, o: Any) -> None:
pass # Nothing to descend into.

def visit_partial_type(self, o: Any) -> None:
raise RuntimeError("Shouldn't get here", o)

Expand Down
Loading