8000 Make strict Optional type system changes standard by ddfisher · Pull Request #3024 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Make strict Optional type system changes standard #3024

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 7 commits into from
Mar 19, 2017
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion mypy/applytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def apply_generic_arguments(callable: CallableType, types: List[Type],

upper_bound = callable.variables[i].upper_bound
if (type and not isinstance(type, PartialType) and
not mypy.subtypes.satisfies_upper_bound(type, upper_bound)):
not mypy.subtypes.is_subtype(type, upper_bound)):
msg.incompatible_typevar_value(callable, i + 1, type, context)

# Create a map from type variable id to target type.
Expand Down
68 changes: 29 additions & 39 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from mypy import nodes
from mypy.types import (
Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType, TypedDictType,
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
Instance, NoneTyp, ErrorType, strip_type, TypeType,
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
true_only, false_only, function_type, is_named_instance
Expand Down Expand Up @@ -300,7 +300,7 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
#
# A classic generator must define a return type that's either
# `Generator[ty, tc, tr]`, Iterator[ty], or Iterable[ty] (or
# object or Any). If tc/tr are not given, both are Void.
# object or Any). If tc/tr are not given, both are None.
#
# A coroutine must define a return type corresponding to tr; the
# other two are unconstrained. The "external" return type (seen
Expand Down Expand Up @@ -377,11 +377,6 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ
# AwaitableGenerator, Generator, AsyncGenerator, Iterator, or Iterable; ty is args[0].
ret_type = return_type.args[0]
# TODO not best fix, better have dedicated yield token
if isinstance(ret_type, NoneTyp):
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 @@ -414,10 +409,7 @@ 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. IOW, tc is None.
if experiments.STRICT_OPTIONAL:
return NoneTyp(is_ret_type=True)
else:
return Void()
return NoneTyp(is_ret_type=True)

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 @@ -529,7 +521,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() in ('__init__', '__init_subclass__') and
not isinstance(typ.ret_type, (Void, NoneTyp)) and
not isinstance(typ.ret_type, NoneTyp) and
not self.dynamic_funcs[-1]):
self.fail(messages.MUST_HAVE_NONE_RETURN_TYPE.format(fdef.name()),
item.type)
Expand Down Expand Up @@ -572,7 +564,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, NoneTyp, AnyType)):
if not isinstance(typ.ret_type.args[2], (NoneTyp, AnyType)):
self.fail(messages.INVALID_GENERATOR_RETURN_ITEM_TYPE, typ)

# Fix the type if decorated with `@types.coroutine` or `@asyncio.coroutine`.
Expand Down Expand Up @@ -660,7 +652,7 @@ def is_implicit_any(t: Type) -> bool:
else:
return_type = self.return_types[-1]

if (not isinstance(return_type, (Void, NoneTyp, AnyType))
if (not isinstance(return_type, (NoneTyp, AnyType))
and not self.is_trivial_body(defn.body)):
# Control flow fell off the end of a function that was
# declared to return a non-None type and is not
Expand Down Expand Up @@ -1149,11 +1141,8 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
partial_types = self.find_partial_types(var)
if partial_types is not None:
if not self.current_node_deferred:
if experiments.STRICT_OPTIONAL:
var.type = UnionType.make_simplified_union(
[rvalue_type, NoneTyp()])
else:
var.type = rvalue_type
var.type = UnionType.make_simplified_union(
[rvalue_type, NoneTyp()])
else:
var.type = None
del partial_types[var]
Expand Down Expand Up @@ -1748,7 +1737,12 @@ def try_infer_partial_type_from_indexed_assignment(
del partial_types[var]

def visit_expression_stmt(self, s: ExpressionStmt) -> None:
self.expr_checker.accept(s.expr)
# Special-case call exprs so we can support warnings on return value of
# None-returning functions
if isinstance(s.expr, CallExpr):
self.expr_checker.accept(s.expr, allow_none_return=True)
else:
self.expr_checker.accept(s.expr)

def visit_return_stmt(self, s: ReturnStmt) -> None:
"""Type check a return statement."""
Expand All @@ -1769,13 +1763,16 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
return

if s.expr:
is_lambda = isinstance(self.scope.top_function(), FuncExpr)
declared_none_return = isinstance(return_type, NoneTyp)
# Return with a value.
typ = self.expr_checker.accept(s.expr, return_type)
typ = self.expr_checker.accept(s.expr,
return_type,
allow_none_return=is_lambda or declared_none_return)

if defn.is_async_generator:
self.fail("'return' with value in async generator is not allowed", s)
return

# Returning a value of type Any is always fine.
if isinstance(typ, AnyType):
# (Unless you asked to be warned in that case, and the
Expand All @@ -1784,10 +1781,10 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
self.warn(messages.RETURN_ANY.format(return_type), s)
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.scope.top_function(), FuncExpr) or isinstance(typ, NoneTyp):
if declared_none_return:
# Lambdas are allowed to have None returns.
# Functions returning a value of type None are allowed to have a None return.
if is_lambda or isinstance(typ, NoneTyp):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unclear.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added some comments

return
self.fail(messages.NO_RETURN_VALUE_EXPECTED, s)
else:
Expand All @@ -1805,7 +1802,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
isinstance(return_type, AnyType)):
return

if isinstance(return_type, (Void, NoneTyp, AnyType)):
if isinstance(return_type, (NoneTyp, AnyType)):
return

if self.in_checked_function():
Expand Down Expand Up @@ -2079,10 +2076,7 @@ def analyze_iterable_item_type(self, expr: Expression) -> Type:

self.check_usable_type(iterable, expr)
if isinstance(iterable, TupleType):
if experiments.STRICT_OPTIONAL:
joined = UninhabitedType() # type: Type
else:
joined = NoneTyp()
joined = UninhabitedType() # type: Type
for item in iterable.items:
joined = join_types(joined, item)
if isinstance(joined, ErrorType):
Expand Down Expand Up @@ -2119,7 +2113,7 @@ def visit_del_stmt(self, s: DelStmt) -> None:
m.line = s.line
c = CallExpr(m, [e.index], [nodes.ARG_POS], [None])
c.line = s.line
c.accept(self.expr_checker)
self.expr_checker.accept(c, allow_none_return=True)
else:
s.expr.accept(self.expr_checker)
for elt in flatten(s.expr):
Expand Down Expand Up @@ -2369,8 +2363,7 @@ def enter_partial_types(self) -> Iterator[None]:
partial_types = self.partial_types.pop()
if not self.current_node_deferred:
for var, context in partial_types.items():
if (experiments.STRICT_OPTIONAL and
isinstance(var.type, PartialType) and var.type.type is None):
if isinstance(var.type, PartialType) and var.type.type is None:
# None partial type: assume variable is intended to have type None
var.type = NoneTyp()
else:
Expand All @@ -2386,9 +2379,9 @@ def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]:
def is_unusable_type(self, typ: Type) -> bool:
"""Is this type an unusable type?

The two unusable types are Void and NoneTyp(is_ret_type=True).
The unusable type is NoneTyp(is_ret_type=True).
"""
return isinstance(typ, Void) or (isinstance(typ, NoneTyp) and typ.is_ret_type)
return 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."""
Expand Down Expand Up @@ -2925,9 +2918,6 @@ def is_valid_inferred_type_component(typ: Type) -> bool:
In strict Optional mode this excludes bare None types, as otherwise every
type containing None would be invalid.
"""
if not experiments.STRICT_OPTIONAL:
if is_same_type(typ, NoneTyp()):
return False
if is_same_type(typ, UninhabitedType()):
return False
elif isinstance(typ, Instance):
Expand Down
47 changes: 21 additions & 26 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from mypy.errors import report_internal_error
from mypy.types import (
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike,
Expand Down Expand Up @@ -173,7 +173,7 @@ def analyze_var_ref(self, var: Var, context: Context) -> Type:
else:
return val

def visit_call_expr(self, e: CallExpr) -> Type:
def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
"""Type check a call expression."""
if e.analyzed:
# It's really a special form that only looks like a call.
Expand All @@ -192,6 +192,8 @@ def visit_call_expr(self, e: CallExpr) -> Type:
ret_type = self.check_call_expr_with_callee_type(callee_type, e)
if isinstance(ret_type, UninhabitedType):
self.chk.binder.unreachable()
if not allow_none_return and isinstance(ret_type, NoneTyp):
self.chk.msg.does_not_return_value(callee_type, e)
return ret_type

def check_typeddict_call(self, callee: TypedDictType,
Expand Down Expand Up @@ -557,9 +559,6 @@ def infer_function_type_arguments_using_context(
for arg in args:
if isinstance(arg, UninhabitedType) or has_erased_component(arg):
new_args.append(None)
elif not experiments.STRICT_OPTIONAL and isinstance(arg, NoneTyp):
# Don't substitute None types in non-strict-Optional mode.
new_args.append(None)
else:
new_args.append(arg)
return self.apply_generic_arguments(callable, new_args, error_context)
Expand Down Expand Up @@ -1192,7 +1191,6 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
if result is None:
result = sub_result
else:
# TODO: check on void needed?
self.check_usable_type(sub_result, e)
result = join.join_types(result, sub_result)

Expand Down Expand Up @@ -1520,20 +1518,12 @@ def visit_enum_index_expr(self, enum_type: TypeInfo, index: Expression,

def visit_cast_expr(self, expr: CastExpr) -> Type:
"""Type check a cast expression."""
source_type = self.accept(expr.expr, type_context=AnyType())
source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True)
target_type = expr.type
if self.chk.options.warn_redundant_casts and is_same_type(source_type, target_type):
self.msg.redundant_cast(target_type, expr)
if not self.is_valid_cast(source_type, target_type):
self.msg.invalid_cast(target_type, source_type, expr)
return target_type

def is_valid_cast(self, source_type: Type, target_type: Type) -> bool:
"""Is a cast from source_type to target_type meaningful?"""
return (isinstance(target_type, AnyType) or
(not isinstance(source_type, Void) and
not isinstance(target_type, Void)))

def visit_reveal_type_expr(self, expr: RevealTypeExpr) -> Type:
"""Type check a reveal_type expression."""
revealed_type = self.accept(expr.expr, type_context=self.type_context[-1])
Expand Down Expand Up @@ -1767,8 +1757,6 @@ def visit_func_expr(self, e: FuncExpr) -> Type:
if not inferred_type:
# No useful type context.
ret_type = self.accept(e.expr())
if isinstance(ret_type, NoneTyp):
ret_type = Void()
fallback = self.named_type('builtins.function')
return callable_type(e, fallback, ret_type)
else:
Expand Down Expand Up @@ -2019,11 +2007,21 @@ def visit_backquote_expr(self, e: BackquoteExpr) -> Type:
# Helpers
#

def accept(self, node: Expression, type_context: Type = None) -> Type:
"""Type check a node in the given type context."""
def accept(self,
node: Expression,
type_context: Type = None,
allow_none_return: bool = False
) -> Type:
"""Type check a node in the given type context. If allow_none_return
is True and this expression is a call, allow it to return None. This
applies only to this expression and not any subexpressions.
"""
self.type_context.append(type_context)
try:
typ = node.accept(self)
if allow_none_return and isinstance(node, CallExpr):
typ = self.visit_call_expr(node, allow_none_return=True)
else:
typ = node.accept(self)
except Exception as err:
report_internal_error(err, self.chk.errors.file,
node.line, self.chk.errors, self.chk.options)
Expand All @@ -2036,7 +2034,7 @@ def accept(self, node: Expression, type_context: Type = None) -> Type:
return typ

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

def named_type(self, name: str) -> Instance:
Expand Down Expand Up @@ -2099,7 +2097,7 @@ def visit_yield_expr(self, e: YieldExpr) -> Type:
return_type = self.chk.return_types[-1]
expected_item_type = self.chk.get_generator_yield_type(return_type, False)
if e.expr is None:
if (not isinstance(expected_item_type, (Void, NoneTyp, AnyType))
if (not isinstance(expected_item_type, (NoneTyp, AnyType))
and self.chk.in_checked_function()):
self.chk.fail(messages.YIELD_VALUE_EXPECTED, e)
else:
Expand Down Expand Up @@ -2184,10 +2182,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
if isinstan F987 ce(actual_item_type, AnyType):
return AnyType()
else:
if experiments.STRICT_OPTIONAL:
return NoneTyp(is_ret_type=True)
else:
return Void()
return NoneTyp(is_ret_type=True)

def visit_temp_node(self, e: TempNode) -> Type:
return e.type
Expand Down
5 changes: 1 addition & 4 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from mypy import experiments
from mypy.types import (
CallableType, Type, TypeVisitor, UnboundType, AnyType, Void, NoneTyp, TypeVarType,
CallableType, Type, TypeVisitor, UnboundType, AnyType, NoneTyp, TypeVarType,
Instance, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType,
DeletedType, UninhabitedType, TypeType, TypeVarId, TypeQuery, ALL_TYPES_STRATEGY,
is_named_instance
Expand Down Expand Up @@ -281,9 +281,6 @@ def visit_unbound_type(self, template: UnboundType) -> List[Constraint]:
def visit_any(self, template: AnyType) -> List[Constraint]:
return []

def visit_void(self, template: Void) -> List[Constraint]:
return []

def visit_none_type(self, template: NoneTyp) -> List[Constraint]:
return []

Expand Down
10 changes: 2 additions & 8 deletions mypy/erasetype.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional, Container, Callable

from mypy.types import (
Type, TypeVisitor, UnboundType, ErrorType, AnyType, Void, NoneTyp, TypeVarId,
Type, TypeVisitor, UnboundType, ErrorType, AnyType, NoneTyp, TypeVarId,
Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded,
ErasedType, PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType
)
Expand Down Expand Up @@ -38,9 +38,6 @@ def visit_type_list(self, t: TypeList) -> Type:
def visit_any(self, t: AnyType) -> Type:
return t

def visit_void(self, t: Void) -> Type:
return t

def visit_none_type(self, t: NoneTyp) -> Type:
return t

Expand All @@ -66,10 +63,7 @@ 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.
if experiments.STRICT_OPTIONAL:
ret_type = NoneTyp(is_ret_type=True) # type: Type
else:
ret_type = Void()
ret_type = NoneTyp(is_ret_type=True) # type: Type
return CallableType([], [], [], ret_type, t.fallback)

def visit_overloaded(self, t: Overloaded) -> Type:
Expand Down
Loading
0