Skip to content

Commit 21e925a

Browse files
committed
Kill Void in strict optional mode
1 parent a95f721 commit 21e925a

File tree

7 files changed

+104
-56
lines changed

7 files changed

+104
-56
lines changed

mypy/checker.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ
316316
ret_type = return_type.args[0]
317317
# TODO not best fix, better have dedicated yield token
318318
if isinstance(ret_type, NoneTyp):
319-
return Void()
319+
if experiments.STRICT_OPTIONAL:
320+
return NoneTyp(is_ret_type=True)
321+
else:
322+
return Void()
320323
return ret_type
321324
else:
322325
# If the function's declared supertype of Generator has no type
@@ -348,7 +351,10 @@ def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> T
348351
else:
349352
# `return_type` is a supertype of Generator, so callers won't be able to send it
350353
# values.
351-
return Void()
354+
if experiments.STRICT_OPTIONAL:
355+
return NoneTyp(is_ret_type=True)
356+
else:
357+
return Void()
352358

353359
def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Type:
354360
"""Given the declared return type of a generator (t), return the type it returns (tr)."""
@@ -492,7 +498,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None:
492498
if fdef:
493499
# Check if __init__ has an invalid, non-None return type.
494500
if (fdef.info and fdef.name() == '__init__' and
495-
not isinstance(typ.ret_type, Void) and
501+
not isinstance(typ.ret_type, (Void, NoneTyp)) and
496502
not self.dynamic_funcs[-1]):
497503
self.fail(messages.INIT_MUST_HAVE_NONE_RETURN_TYPE,
498504
item.type)
@@ -531,8 +537,7 @@ def is_implicit_any(t: Type) -> bool:
531537
if (self.options.python_version[0] == 2 and
532538
isinstance(typ.ret_type, Instance) and
533539
typ.ret_type.type.fullname() == 'typing.Generator'):
534-
if not (isinstance(typ.ret_type.args[2], Void)
535-
or isinstance(typ.ret_type.args[2], AnyType)):
540+
if not isinstance(typ.ret_type.args[2], (Void, NoneTyp, AnyType)):
536541
self.fail(messages.INVALID_GENERATOR_RETURN_ITEM_TYPE, typ)
537542

538543
# Push return type.
@@ -1270,8 +1275,8 @@ def infer_variable_type(self, name: Var, lvalue: Node,
12701275
if self.typing_mode_weak():
12711276
self.set_inferred_type(name, lvalue, AnyType())
12721277
self.binder.assign_type(lvalue, init_type, self.binder.get_declaration(lvalue), True)
1273-
elif isinstance(init_type, Void):
1274-
self.check_not_void(init_type, context)
1278+
elif self.is_unusable_type(init_type):
1279+
self.check_usable_type(init_type, context)
12751280
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
12761281
elif isinstance(init_type, DeletedType):
12771282
self.msg.deleted_as_rvalue(init_type, context)
@@ -1429,8 +1434,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> Type:
14291434
if isinstance(typ, AnyType):
14301435
return None
14311436

1432-
if isinstance(return_type, Void):
1433-
# Lambdas are allowed to have a Void return.
1437+
if self.is_unusable_type(return_type):
1438+
# Lambdas are allowed to have a unusable returns.
14341439
# Functions returning a value of type None are allowed to have a Void return.
14351440
if isinstance(self.function_stack[-1], FuncExpr) or isinstance(typ, NoneTyp):
14361441
return None
@@ -1488,7 +1493,7 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
14881493
with self.binder.frame_context():
14891494
for e, b in zip(s.expr, s.body):
14901495
t = self.accept(e)
1491-
self.check_not_void(t, e)
1496+
self.check_usable_type(t, e)
14921497
if_map, else_map = find_isinstance_check(
14931498
e, self.type_map,
14941499
self.typing_mode_weak()
@@ -1696,7 +1701,7 @@ def analyze_async_iterable_item_type(self, expr: Node) -> Type:
16961701
"""Analyse async iterable expression and return iterator item type."""
16971702
iterable = self.accept(expr)
16981703

1699-
self.check_not_void(iterable, expr)
1704+
self.check_usable_type(iterable, expr)
17001705

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

1718-
self.check_not_void(iterable, expr)
1723+
self.check_usable_type(iterable, expr)
17191724
if isinstance(iterable, TupleType):
17201725
if experiments.STRICT_OPTIONAL:
17211726
joined = UninhabitedType() # type: Type
@@ -1912,7 +1917,10 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
19121917
if isinstance(actual_item_type, AnyType):
19131918
return AnyType()
19141919
else:
1915-
return Void()
1920+
if experiments.STRICT_OPTIONAL:
1921+
return NoneTyp(is_ret_type=True)
1922+
else:
1923+
return Void()
19161924

19171925
def visit_member_expr(self, e: MemberExpr) -> Type:
19181926
return self.expr_checker.visit_member_expr(e)
@@ -2029,8 +2037,7 @@ def visit_yield_expr(self, e: YieldExpr) -> Type:
20292037
return_type = self.return_types[-1]
20302038
expected_item_type = self.get_generator_yield_type(return_type, False)
20312039
if e.expr is None:
2032-
if (not (isinstance(expected_item_type, Void) or
2033-
isinstance(expected_item_type, AnyType))
2040+
if (not isinstance(expected_item_type, (Void, NoneTyp, AnyType))
20342041
and self.typing_mode_full()):
20352042
self.fail(messages.YIELD_VALUE_EXPECTED, e)
20362043
else:
@@ -2062,7 +2069,7 @@ def check_subtype(self, subtype: Type, supertype: Type, context: Context,
20622069
if is_subtype(subtype, supertype):
20632070
return True
20642071
else:
2065-
if isinstance(subtype, Void):
2072+
if self.is_unusable_type(subtype):
20662073
self.msg.does_not_return_value(subtype, context)
20672074
else:
20682075
extra_info = [] # type: List[str]
@@ -2211,9 +2218,16 @@ def is_within_function(self) -> bool:
22112218
"""
22122219
return self.return_types != []
22132220

2214-
def check_not_void(self, typ: Type, context: Context) -> None:
2215-
"""Generate an error if the type is Void."""
2216-
if isinstance(typ, Void):
2221+
def is_unusable_type(self, typ: Type):
2222+
"""Is this type an unusable type?
2223+
2224+
The two unusable types are Void and NoneTyp(is_ret_type=True).
2225+
"""
2226+
return isinstance(typ, Void) or (isinstance(typ, NoneTyp) and typ.is_ret_type)
2227+
2228+
def check_usable_type(self, typ: Type, context: Context) -> None:
2229+
"""Generate an error if the type is not a usable type."""
2230+
if self.is_unusable_type(typ):
22172231
self.msg.does_not_return_value(typ, context)
22182232

22192233
def temp_node(self, t: Type, context: Context = None) -> Node:

mypy/checkexpr.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
702702
callee_type: Type, n: int, m: int, callee: CallableType,
703703
context: Context, messages: MessageBuilder) -> None:
704704
"""Check the type of a single argument in a call."""
705-
if isinstance(caller_type, Void):
705+
if self.chk.is_unusable_type(caller_type):
706706
messages.does_not_return_value(caller_type, context)
707707
elif isinstance(caller_type, DeletedType):
708708
messages.deleted_as_rvalue(caller_type, context)
@@ -997,7 +997,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
997997
result = sub_result
998998
else:
999999
# TODO: check on void needed?
1000-
self.check_not_void(sub_result, e)
1000+
self.check_usable_type(sub_result, e)
10011001
result = join.join_types(result, sub_result)
10021002

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

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

1119-
self.check_not_void(left_type, context)
1120-
self.check_not_void(right_type, context)
1119+
self.check_usable_type(left_type, context)
1120+
self.check_usable_type(right_type, context)
11211121

11221122
# If either of the type maps is None that means that result cannot happen.
11231123
# If both of the type maps are None we just have no information.
@@ -1148,7 +1148,7 @@ def visit_unary_expr(self, e: UnaryExpr) -> Type:
11481148
operand_type = self.accept(e.expr)
11491149
op = e.op
11501150
if op == 'not':
1151-
self.check_not_void(operand_type, e)
1151+
self.check_usable_type(operand_type, e)
11521152
result = self.chk.bool_type() # type: Type
11531153
elif op == '-':
11541154
method_type = self.analyze_external_member_access('__neg__',
@@ -1340,7 +1340,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
13401340
# context? Counterargument: Why would anyone write
13411341
# (1, *(2, 3)) instead of (1, 2, 3) except in a test?
13421342
tt = self.accept(item.expr)
1343-
self.check_not_void(tt, e)
1343+
self.check_usable_type(tt, e)
13441344
if isinstance(tt, TupleType):
13451345
items.extend(tt.items)
13461346
j += len(tt.items)
@@ -1354,7 +1354,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
13541354
else:
13551355
tt = self.accept(item, ctx.items[j])
13561356
j += 1
1357-
self.check_not_void(tt, e)
1357+
self.check_usable_type(tt, e)
13581358
items.append(tt)
13591359
fallback_item = join.join_type_list(items)
13601360
return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item]))
@@ -1581,7 +1581,7 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No
15811581

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

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

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

16411641
def is_boolean(self, typ: Type) -> bool:
16421642
"""Is type compatible with bool?"""

mypy/erasetype.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType,
66
PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType
77
)
8+
from mypy import experiments
89

910

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

6667
def visit_callable_type(self, t: CallableType) -> Type:
6768
# We must preserve the fallback type for overload resolution to work.
68-
return CallableType([], [], [], Void(), t.fallback)
69+
if experiments.STRICT_OPTIONAL:
70+
ret_type = NoneTyp(is_ret_type=True) # type: Type
71+
else:
72+
ret_type = Void()
73+
return CallableType([], [], [], ret_type, t.fallback)
6974

7075
def visit_overloaded(self, t: Overloaded) -> Type:
7176
return t.items()[0].accept(self)

mypy/messages.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ def has_no_attr(self, typ: Type, member: str, context: Context) -> Type:
320320
if (isinstance(typ, Instance) and
321321
typ.type.has_readable_member(member)):
322322
self.fail('Member "{}" is not assignable'.format(member), context)
323-
elif isinstance(typ, Void):
324-
self.check_void(typ, context)
323+
elif self.check_unusable_type(typ, context):
324+
pass
325325
elif member == '__contains__':
326326
self.fail('Unsupported right operand type for in ({})'.format(
327327
self.format(typ)), context)
@@ -376,9 +376,8 @@ def unsupported_operand_types(self, op: str, left_type: Any,
376376
377377
Types can be Type objects or strings.
378378
"""
379-
if isinstance(left_type, Void) or isinstance(right_type, Void):
380-
self.check_void(left_type, context)
381-
self.check_void(right_type, context)
379+
if (self.check_unusable_type(left_type, context) or
380+
self.check_unusable_type(right_type, context)):
382381
return
383382
left_str = ''
384383
if isinstance(left_type, str):
@@ -401,7 +400,7 @@ def unsupported_operand_types(self, op: str, left_type: Any,
401400

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

557-
def does_not_return_value(self, void_type: Type, context: Context) -> None:
558-
"""Report an error about a void type in a non-void context.
556+
def does_not_return_value(self, unusable_type: Type, context: Context) -> None:
557+
"""Report an error about use of an unusable type.
559558
560-
The first argument must be a void type. If the void type has a
561-
source in it, report it in the error message. This allows
562-
giving messages such as 'Foo does not return a value'.
559+
If the type is a Void type and has a source in it, report it in the error message.
560+
This allows giving messages such as 'Foo does not return a value'.
563561
"""
564-
if (cast(Void, void_type)).source is None:
565-
self.fail('Function does not return a value', context)
566-
else:
562+
if isinstance(unusable_type, Void) and unusable_type.source is not None:
567563
self.fail('{} does not return a value'.format(
568-
capitalize((cast(Void, void_type)).source)), context)
564+
capitalize((cast(Void, unusable_type)).source)), context)
565+
else:
566+
self.fail('Function does not return a value', context)
569567

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

603601
def invalid_cast(self, target_type: Type, source_type: Type,
604602
context: Context) -> None:
605-
if not self.check_void(source_type, context):
603+
if not self.check_unusable_type(source_type, context):
606604
self.fail('Cannot cast from {} to {}'.format(
607605
self.format(source_type), self.format(target_type)), context)
608606

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

723-
def check_void(self, typ: Type, context: Context) -> bool:
724-
"""If type is void, report an error such as '.. does not
721+
def check_unusable_type(self, typ: Type, context: Context) -> bool:
722+
"""If type is a type which is not meant to be used (like Void or
723+
NoneTyp(is_ret_type=True)), report an error such as '.. does not
725724
return a value' and return True. Otherwise, return False.
726725
"""
727-
if isinstance(typ, Void):
726+
if (isinstance(typ, Void) or
727+
(isinstance(typ, NoneTyp) and typ.is_ret_type)):
728728
self.does_not_return_value(typ, context)
729729
return True
730730
else:

mypy/typeanal.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
9595
return TypeVarType(sym.tvar_def, t.line)
9696
elif fullname == 'builtins.None':
9797
if experiments.STRICT_OPTIONAL:
98-
if t.is_ret_type:
99-
return Void()
100-
else:
101-
return NoneTyp()
98+
return NoneTyp(is_ret_type=t.is_ret_type)
10299
else:
103100
return Void()
104101
elif fullname == 'typing.Any':

mypy/types.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,19 +327,22 @@ class NoneTyp(Type):
327327
of a function, where 'None' means Void.
328328
"""
329329

330-
def __init__(self, line: int = -1) -> None:
330+
def __init__(self, is_ret_type: bool = False, line: int = -1) -> None:
331331
super().__init__(line)
332+
self.is_ret_type = is_ret_type
332333

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

336337
def serialize(self) -> JsonDict:
337-
return {'.class': 'NoneTyp'}
338+
return {'.class': 'NoneTyp',
339+
'is_ret_type': self.is_ret_type,
340+
}
338341

339342
@classmethod
340343
def deserialize(self, data: JsonDict) -> 'NoneTyp':
341344
assert data['.class'] == 'NoneTyp'
342-
return NoneTyp()
345+
return NoneTyp(is_ret_type=data['is_ret_type'])
343346

344347

345348
class ErasedType(Type):

test-data/unit/check-optional.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,32 @@ from typing import overload
274274
def f() -> None: ...
275275
@overload
276276
def f(o: object) -> None: ...
277+
278+
[case testGenericSubclassReturningNone]
279+
from typing import Generic, TypeVar
280+
281+
T = TypeVar('T')
282+
283+
class Base(Generic[T]):
284+
def f(self) -> T:
285+
pass
286+
287+
class SubNone(Base[None]):
288+
def f(self) -> None:
289+
pass
290+
291+
class SubInt(Base[int]):
292+
def f(self) -> int:
293+
return 1
294+
295+
[case testUseOfNoneReturningFunction]
296+
from typing import Optional
297+
def f() -> None:
298+
pass
299+
300+
def g(x: Optional[int]) -> int:
301+
pass
302+
303+
x = f() # E: Function does not return a value
304+
f() + 1 # E: Function does not return a value
305+
g(f()) # E: Function does not return a value

0 commit comments

Comments
 (0)