Skip to content

Kill Void in strict optional mode #1980

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
Aug 3, 2016
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
52 changes: 33 additions & 19 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ
ret_type = return_type.args[0]
# TODO not best fix, better have dedicated yield token
if isinstance(ret_type, NoneTyp):
return Void()
if experiments.STRICT_OPTIONAL:
return NoneTyp(is_ret_type=True)
else:
return Void()
return ret_type
else:
# If the function's declared supertype of Generator has no type
Expand Down Expand Up @@ -348,7 +351,10 @@ def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> T
else:
# `return_type` is a supertype of Generator, so callers won't be able to send it
# values.
return Void()
if experiments.STRICT_OPTIONAL:
return NoneTyp(is_ret_type=True)
else:
return Void()

def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Type:
"""Given the declared return type of a generator (t), return the type it returns (tr)."""
Expand Down Expand Up @@ -492,7 +498,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None:
if fdef:
# Check if __init__ has an invalid, non-None return type.
if (fdef.info and fdef.name() == '__init__' and
not isinstance(typ.ret_type, Void) and
not isinstance(typ.ret_type, (Void, NoneTyp)) and
not self.dynamic_funcs[-1]):
self.fail(messages.INIT_MUST_HAVE_NONE_RETURN_TYPE,
item.type)
Expand Down Expand Up @@ -531,8 +537,7 @@ def is_implicit_any(t: Type) -> bool:
if (self.options.python_version[0] == 2 and
isinstance(typ.ret_type, Instance) and
typ.ret_type.type.fullname() == 'typing.Generator'):
if not (isinstance(typ.ret_type.args[2], Void)
or isinstance(typ.ret_type.args[2], AnyType)):
if not isinstance(typ.ret_type.args[2], (Void, NoneTyp, AnyType)):
self.fail(messages.INVALID_GENERATOR_RETURN_ITEM_TYPE, typ)

# Push return type.
Expand Down Expand Up @@ -1270,8 +1275,8 @@ def infer_variable_type(self, name: Var, lvalue: Node,
if self.typing_mode_weak():
self.set_inferred_type(name, lvalue, AnyType())
self.binder.assign_type(lvalue, init_type, self.binder.get_declaration(lvalue), True)
elif isinstance(init_type, Void):
self.check_not_void(init_type, context)
elif self.is_unusable_type(init_type):
self.check_usable_type(init_type, context)
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
elif isinstance(init_type, DeletedType):
self.msg.deleted_as_rvalue(init_type, context)
Expand Down Expand Up @@ -1429,8 +1434,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> Type:
if isinstance(typ, AnyType):
return None

if isinstance(return_type, Void):
# Lambdas are allowed to have a Void return.
if self.is_unusable_type(return_type):
# Lambdas are allowed to have a unusable returns.
# Functions returning a value of type None are allowed to have a Void return.
if isinstance(self.function_stack[-1], FuncExpr) or isinstance(typ, NoneTyp):
return None
Expand Down Expand Up @@ -1488,7 +1493,7 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
with self.binder.frame_context():
for e, b in zip(s.expr, s.body):
t = self.accept(e)
self.check_not_void(t, e)
self.check_usable_type(t, e)
if_map, else_map = find_isinstance_check(
e, self.type_map,
self.typing_mode_weak()
Expand Down Expand Up @@ -1696,7 +1701,7 @@ def analyze_async_iterable_item_type(self, expr: Node) -> Type:
"""Analyse async iterable expression and return iterator item type."""
iterable = self.accept(expr)

self.check_not_void(iterable, expr)
self.check_usable_type(iterable, expr)

self.check_subtype(iterable,
self.named_generic_type('typing.AsyncIterable',
Expand All @@ -1715,7 +1720,7 @@ def analyze_iterable_item_type(self, expr: Node) -> Type:
"""Analyse iterable expression and return iterator item type."""
iterable = self.accept(expr)

self.check_not_void(iterable, expr)
self.check_usable_type(iterable, expr)
if isinstance(iterable, TupleType):
if experiments.STRICT_OPTIONAL:
joined = UninhabitedType() # type: Type
Expand Down Expand Up @@ -1912,7 +1917,10 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
if isinstance(actual_item_type, AnyType):
return AnyType()
else:
return Void()
if experiments.STRICT_OPTIONAL:
return NoneTyp(is_ret_type=True)
else:
return Void()

def visit_member_expr(self, e: MemberExpr) -> Type:
return self.expr_checker.visit_member_expr(e)
Expand Down Expand Up @@ -2029,8 +2037,7 @@ def visit_yield_expr(self, e: YieldExpr) -> Type:
return_type = self.return_types[-1]
expected_item_type = self.get_generator_yield_type(return_type, False)
if e.expr is None:
if (not (isinstance(expected_item_type, Void) or
isinstance(expected_item_type, AnyType))
if (not isinstance(expected_item_type, (Void, NoneTyp, AnyType))
and self.typing_mode_full()):
self.fail(messages.YIELD_VALUE_EXPECTED, e)
else:
Expand Down Expand Up @@ -2062,7 +2069,7 @@ def check_subtype(self, subtype: Type, supertype: Type, context: Context,
if is_subtype(subtype, supertype):
return True
else:
if isinstance(subtype, Void):
if self.is_unusable_type(subtype):
self.msg.does_not_return_value(subtype, context)
else:
extra_info = [] # type: List[str]
Expand Down Expand Up @@ -2211,9 +2218,16 @@ def is_within_function(self) -> bool:
"""
return self.return_types != []

def check_not_void(self, typ: Type, context: Context) -> None:
"""Generate an error if the type is Void."""
if isinstance(typ, Void):
def is_unusable_type(self, typ: Type):
"""Is this type an unusable type?

The two unusable types are Void and NoneTyp(is_ret_type=True).
"""
return isinstance(typ, Void) or (isinstance(typ, NoneTyp) and typ.is_ret_type)

def check_usable_type(self, typ: Type, context: Context) -> None:
"""Generate an error if the type is not a usable type."""
if self.is_unusable_type(typ):
self.msg.does_not_return_value(typ, context)

def temp_node(self, t: Type, context: Context = None) -> Node:
Expand Down
20 changes: 10 additions & 10 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
callee_type: Type, n: int, m: int, callee: CallableType,
context: Context, messages: MessageBuilder) -> None:
"""Check the type of a single argument in a call."""
if isinstance(caller_type, Void):
if self.chk.is_unusable_type(caller_type):
messages.does_not_return_value(caller_type, context)
elif isinstance(caller_type, DeletedType):
messages.deleted_as_rvalue(caller_type, context)
Expand Down Expand Up @@ -997,7 +997,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
result = sub_result
else:
# TODO: check on void needed?
self.check_not_void(sub_result, e)
self.check_usable_type(sub_result, e)
result = join.join_types(result, sub_result)

return result
Expand Down Expand Up @@ -1116,8 +1116,8 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:

right_type = self.accept(e.right, left_type)

self.check_not_void(left_type, context)
self.check_not_void(right_type, context)
self.check_usable_type(left_type, context)
self.check_usable_type(right_type, context)

# If either of the type maps is None that means that result cannot happen.
# If both of the type maps are None we just have no information.
Expand Down Expand Up @@ -1148,7 +1148,7 @@ def visit_unary_expr(self, e: UnaryExpr) -> Type:
operand_type = self.accept(e.expr)
op = e.op
if op == 'not':
self.check_not_void(operand_type, e)
self.check_usable_type(operand_type, e)
result = self.chk.bool_type() # type: Type
elif op == '-':
method_type = self.analyze_external_member_access('__neg__',
Expand Down Expand Up @@ -1340,7 +1340,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
# context? Counterargument: Why would anyone write
# (1, *(2, 3)) instead of (1, 2, 3) except in a test?
tt = self.accept(item.expr)
self.check_not_void(tt, e)
self.check_usable_type(tt, e)
if isinstance(tt, TupleType):
items.extend(tt.items)
j += len(tt.items)
Expand All @@ -1354,7 +1354,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
else:
tt = self.accept(item, ctx.items[j])
j += 1
self.check_not_void(tt, e)
self.check_usable_type(tt, e)
items.append(tt)
fallback_item = join.join_type_list(items)
return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item]))
Expand Down Expand Up @@ -1581,7 +1581,7 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No

def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
cond_type = self.accept(e.cond)
self.check_not_void(cond_type, e)
self.check_usable_type(cond_type, e)
ctx = self.chk.type_context[-1]

# Gain type information from isinstance if it is there
Expand Down Expand Up @@ -1634,9 +1634,9 @@ def accept(self, node: Node, context: Type = None) -> Type:
"""Type check a node. Alias for TypeChecker.accept."""
return self.chk.accept(node, context)

def check_not_void(self, typ: Type, context: Context) -> None:
def check_usable_type(self, typ: Type, context: Context) -> None:
"""Generate an error if type is Void."""
self.chk.check_not_void(typ, context)
self.chk.check_usable_type(typ, context)

def is_boolean(self, typ: Type) -> bool:
"""Is type compatible with bool?"""
Expand Down
7 changes: 6 additions & 1 deletion mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType,
PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType
)
from mypy import experiments


def erase_type(typ: Type) -> Type:
Expand Down Expand Up @@ -65,7 +66,11 @@ def visit_type_var(self, t: TypeVarType) -> Type:

def visit_callable_type(self, t: CallableType) -> Type:
# We must preserve the fallback type for overload resolution to work.
return CallableType([], [], [], Void(), t.fallback)
if experiments.STRICT_OPTIONAL:
ret_type = NoneTyp(is_ret_type=True) # type: Type
else:
ret_type = Void()
return CallableType([], [], [], ret_type, t.fallback)

def visit_overloaded(self, t: Overloaded) -> Type:
return t.items()[0].accept(self)
Expand Down
38 changes: 19 additions & 19 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ def has_no_attr(self, typ: Type, member: str, context: Context) -> Type:
if (isinstance(typ, Instance) and
typ.type.has_readable_member(member)):
self.fail('Member "{}" is not assignable'.format(member), context)
elif isinstance(typ, Void):
self.check_void(typ, context)
elif self.check_unusable_type(typ, context):
pass
elif member == '__contains__':
self.fail('Unsupported right operand type for in ({})'.format(
self.format(typ)), context)
Expand Down Expand Up @@ -376,9 +376,8 @@ def unsupported_operand_types(self, op: str, left_type: Any,

Types can be Type objects or strings.
"""
if isinstance(left_type, Void) or isinstance(right_type, Void):
self.check_void(left_type, context)
self.check_void(right_type, context)
if (self.check_unusable_type(left_type, context) or
self.check_unusable_type(right_type, context)):
return
left_str = ''
if isinstance(left_type, str):
Expand All @@ -401,7 +400,7 @@ def unsupported_operand_types(self, op: str, left_type: Any,

def unsupported_left_operand(self, op: str, typ: Type,
context: Context) -> None:
if not self.check_void(typ, context):
if not self.check_unusable_type(typ, context):
if self.disable_type_names:
msg = 'Unsupported left operand type for {} (some union)'.format(op)
else:
Expand Down Expand Up @@ -554,18 +553,17 @@ def duplicate_argument_value(self, callee: CallableType, index: int,
format(capitalize(callable_name(callee)),
callee.arg_names[index]), context)

def does_not_return_value(self, void_type: Type, context: Context) -> None:
"""Report an error about a void type in a non-void context.
def does_not_return_value(self, unusable_type: Type, context: Context) -> None:
"""Report an error about use of an unusable type.

The first argument must be a void type. If the void type has a
source in it, report it in the error message. This allows
giving messages such as 'Foo does not return a value'.
If the type is a Void type and has a source in it, report it in the error message.
This allows giving messages such as 'Foo does not return a value'.
"""
if (cast(Void, void_type)).source is None:
self.fail('Function does not return a value', context)
else:
if isinstance(unusable_type, Void) and unusable_type.source is not None:
self.fail('{} does not return a value'.format(
capitalize((cast(Void, void_type)).source)), context)
capitalize((cast(Void, unusable_type)).source)), context)
else:
self.fail('Function does not return a value', context)

def deleted_as_rvalue(self, typ: DeletedType, context: Context) -> None:
"""Report an error about using an deleted type as an rvalue."""
Expand Down Expand Up @@ -602,7 +600,7 @@ def function_variants_overlap(self, n1: int, n2: int,

def invalid_cast(self, target_type: Type, source_type: Type,
context: Context) -> None:
if not self.check_void(source_type, context):
if not self.check_unusable_type(source_type, context):
self.fail('Cannot cast from {} to {}'.format(
self.format(source_type), self.format(target_type)), context)

Expand Down Expand Up @@ -720,11 +718,13 @@ def not_implemented(self, msg: str, context: Context) -> Type:
def undefined_in_superclass(self, member: str, context: Context) -> None:
self.fail('"{}" undefined in superclass'.format(member), context)

def check_void(self, typ: Type, context: Context) -> bool:
"""If type is void, report an error such as '.. does not
def check_unusable_type(self, typ: Type, context: Context) -> bool:
"""If type is a type which is not meant to be used (like Void or
NoneTyp(is_ret_type=True)), report an error such as '.. does not
return a value' and return True. Otherwise, return False.
"""
if isinstance(typ, Void):
if (isinstance(typ, Void) or
(isinstance(typ, NoneTyp) and typ.is_ret_type)):
self.does_not_return_value(typ, context)
return True
else:
Expand Down
5 changes: 1 addition & 4 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
return TypeVarType(sym.tvar_def, t.line)
elif fullname == 'builtins.None':
if experiments.STRICT_OPTIONAL:
if t.is_ret_type:
return Void()
else:
return NoneTyp()
return NoneTyp(is_ret_type=t.is_ret_type)
else:
return Void()
elif fullname == 'typing.Any':
Expand Down
9 changes: 6 additions & 3 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,19 +327,22 @@ class NoneTyp(Type):
of a function, where 'None' means Void.
"""

def __init__(self, line: int = -1) -> None:
def __init__(self, is_ret_type: bool = False, line: int = -1) -> None:
super().__init__(line)
self.is_ret_type = is_ret_type

def accept(self, visitor: 'TypeVisitor[T]') -> T:
return visitor.visit_none_type(self)

def serialize(self) -> JsonDict:
return {'.class': 'NoneTyp'}
return {'.class': 'NoneTyp',
'is_ret_type': self.is_ret_type,
}

@classmethod
def deserialize(self, data: JsonDict) -> 'NoneTyp':
assert data['.class'] == 'NoneTyp'
return NoneTyp()
return NoneTyp(is_ret_type=data['is_ret_type'])


class ErasedType(Type):
Expand Down
Loading