Skip to content

Implement per-file strict Optional #3206

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 9 commits into from
Jun 28, 2017
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
101 changes: 59 additions & 42 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,54 +177,56 @@ def check_first_pass(self) -> None:

Deferred functions will be processed by check_second_pass().
"""
self.errors.set_file(self.path, self.tree.fullname())
with self.enter_partial_types():
with self.binder.top_frame_context():
for d in self.tree.defs:
self.accept(d)
with experiments.strict_optional_set(self.options.strict_optional):
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be better to wrap the call sites in such a context manager? Fewer lines in the diff, and there's only one call site in build.py (and another in server/update.py which is only used by fine-grained incrementalism). Ditto for check_second_pass() and visit_file() -- these are each only called from one place in build.py and perhaps another place in server/.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure how likely it is that we ever add more call sites, but I think that's a bug in the making. The context manager should always wrap this code -- I don't think there's any case where we'd want to call this while ignoring the per-file Strict Optional setting. I understand that you're not a huge fan of code churn, but I don't think it'd be a worthwhile tradeoff in this instance.

Also, FWIW git blame has the -w flag which ignores whitespace changes, so this doesn't have to mess up anyone's blame.

Copy link
Member

Choose a reason for hiding this comment

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

Hm. git might have that flag but GitHub doesn't (as shown in this code review).

Maybe you can make it a decorator containing a context manager?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It looks like that won't quite work, but apparently github has a -w equivalent too! https://github.com/python/mypy/pull/3206/files?w=1

Copy link
Member

Choose a reason for hiding this comment

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

Hm, the -w flag is flawed -- it doesn't show review comments, and it's not sticky.

self.errors.set_file(self.path, self.tree.fullname())
with self.enter_partial_types():
with self.binder.top_frame_context():
for d in self.tree.defs:
self.accept(d)

assert not self.current_node_deferred
assert not self.current_node_deferred

all_ = self.globals.get('__all__')
if all_ is not None and all_.type is not None:
all_node = all_.node
assert all_node is not None
seq_str = self.named_generic_type('typing.Sequence',
[self.named_type('builtins.str')])
if self.options.python_version[0] < 3:
all_ = self.globals.get('__all__')
if all_ is not None and all_.type is not None:
all_node = all_.node
assert all_node is not None
seq_str = self.named_generic_type('typing.Sequence',
[self.named_type('builtins.unicode')])
if not is_subtype(all_.type, seq_str):
str_seq_s, all_s = self.msg.format_distinctly(seq_str, all_.type)
self.fail(messages.ALL_MUST_BE_SEQ_STR.format(str_seq_s, all_s),
all_node)
[self.named_type('builtins.str')])
if self.options.python_version[0] < 3:
seq_str = self.named_generic_type('typing.Sequence',
[self.named_type('builtins.unicode')])
if not is_subtype(all_.type, seq_str):
str_seq_s, all_s = self.msg.format_distinctly(seq_str, all_.type)
self.fail(messages.ALL_MUST_BE_SEQ_STR.format(str_seq_s, all_s),
all_node)

def check_second_pass(self, todo: List[DeferredNode] = None) -> bool:
"""Run second or following pass of type checking.

This goes through deferred nodes, returning True if there were any.
"""
if not todo and not self.deferred_nodes:
return False
self.errors.set_file(self.path, self.tree.fullname())
self.pass_num += 1
if not todo:
todo = self.deferred_nodes
else:
assert not self.deferred_nodes
self.deferred_nodes = []
done = set() # type: Set[Union[FuncDef, LambdaExpr, MypyFile]]
for node, type_name, active_typeinfo in todo:
if node in done:
continue
# This is useful for debugging:
# print("XXX in pass %d, class %s, function %s" %
# (self.pass_num, type_name, node.fullname() or node.name()))
done.add(node)
with self.errors.enter_type(type_name) if type_name else nothing():
with self.scope.push_class(active_typeinfo) if active_typeinfo else nothing():
self.check_partial(node)
return True
with experiments.strict_optional_set(self.options.strict_optional):
if not todo and not self.deferred_nodes:
return False
self.errors.set_file(self.path, self.tree.fullname())
self.pass_num += 1
if not todo:
todo = self.deferred_nodes
else:
assert not self.deferred_nodes
self.deferred_nodes = []
done = set() # type: Set[Union[FuncDef, LambdaExpr, MypyFile]]
for node, type_name, active_typeinfo in todo:
if node in done:
continue
# This is useful for debugging:
# print("XXX in pass %d, class %s, function %s" %
# (self.pass_num, type_name, node.fullname() or node.name()))
done.add(node)
with self.errors.enter_type(type_name) if type_name else nothing():
with self.scope.push_class(active_typeinfo) if active_typeinfo else nothing():
self.check_partial(node)
return True

def check_partial(self, node: Union[FuncDef, LambdaExpr, MypyFile]) -> None:
if isinstance(node, MypyFile):
Expand Down Expand Up @@ -1498,6 +1500,12 @@ def check_multi_assignment(self, lvalues: List[Lvalue],
rvalue_type = self.expr_checker.accept(rvalue) # TODO maybe elsewhere; redundant
undefined_rvalue = False

if isinstance(rvalue_type, UnionType):
# If this is an Optional type in non-strict Optional code, unwrap it.
relevant_items = rvalue_type.relevant_items()
if len(relevant_items) == 1:
rvalue_type = relevant_items[0]

if isinstance(rvalue_type, AnyType):
for lv in lvalues:
if isinstance(lv, StarExpr):
Expand Down Expand Up @@ -1525,7 +1533,16 @@ def check_multi_assignment_from_tuple(self, lvalues: List[Lvalue], rvalue: Expre
if not undefined_rvalue:
# Infer rvalue again, now in the correct type context.
lvalue_type = self.lvalue_type_for_inference(lvalues, rvalue_type)
rvalue_type = cast(TupleType, self.expr_checker.accept(rvalue, lvalue_type))
reinferred_rvalue_type = self.expr_checker.accept(rvalue, lvalue_type)

if isinstance(reinferred_rvalue_type, UnionType):
# If this is an Optional type in non-strict Optional code, unwrap it.
relevant_items = reinferred_rvalue_type.relevant_items()
if len(relevant_items) == 1:
reinferred_rvalue_type = relevant_items[0]

assert isinstance(reinferred_rvalue_type, TupleType)
rvalue_type = reinferred_rvalue_type

left_rv_types, star_rv_types, right_rv_types = self.split_around_star(
rvalue_type.items, star_index, len(lvalues))
Expand Down Expand Up @@ -2161,7 +2178,7 @@ def get_types_from_except_handler(self, typ: Type, n: Expression) -> List[Type]:
elif isinstance(typ, UnionType):
return [
union_typ
for item in typ.items
for item in typ.relevant_items()
for union_typ in self.get_types_from_except_handler(item, n)
]
elif isinstance(typ, Instance) and is_named_instance(typ, 'builtins.tuple'):
Expand Down Expand Up @@ -2627,7 +2644,7 @@ def partition_by_callable(type: Optional[Type]) -> Tuple[List[Type], List[Type]]
if isinstance(type, UnionType):
callables = []
uncallables = []
for subtype in type.items:
for subtype in type.relevant_items():
subcallables, subuncallables = partition_by_callable(subtype)
callables.extend(subcallables)
uncallables.extend(subuncallables)
Expand Down
12 changes: 6 additions & 6 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ def check_call(self, callee: Type, args: List[Expression],
self.msg.disable_type_names += 1
results = [self.check_call(subtype, args, arg_kinds, context, arg_names,
arg_messages=arg_messages)
for subtype in callee.items]
for subtype in callee.relevant_items()]
self.msg.disable_type_names -= 1
return (UnionType.make_simplified_union([res[0] for res in results]),
callee)
Expand Down Expand Up @@ -596,7 +596,7 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
return res
if isinstance(item, UnionType):
return UnionType([self.analyze_type_type_callee(item, context)
for item in item.items], item.line)
for item in item.relevant_items()], item.line)
if isinstance(item, TypeVarType):
# Pretend we're calling the typevar's upper bound,
# i.e. its constructor (a poor approximation for reality,
Expand Down Expand Up @@ -1982,7 +1982,7 @@ def infer_lambda_type_using_context(self, e: LambdaExpr) -> Tuple[Optional[Calla
ctx = self.type_context[-1]

if isinstance(ctx, UnionType):
callables = [t for t in ctx.items if isinstance(t, CallableType)]
callables = [t for t in ctx.relevant_items() if isinstance(t, CallableType)]
if len(callables) == 1:
ctx = callables[0]

Expand Down Expand Up @@ -2284,7 +2284,7 @@ def has_member(self, typ: Type, member: str) -> bool:
elif isinstance(typ, AnyType):
return True
elif isinstance(typ, UnionType):
result = all(self.has_member(x, member) for x in typ.items)
result = all(self.has_member(x, member) for x in typ.relevant_items())
return result
elif isinstance(typ, TupleType):
return self.has_member(typ.fallback, member)
Expand Down Expand Up @@ -2659,10 +2659,10 @@ def overload_arg_similarity(actual: Type, formal: Type) -> int:
return 2
if isinstance(actual, UnionType):
return max(overload_arg_similarity(item, formal)
for item in actual.items)
for item in actual.relevant_items())
if isinstance(formal, UnionType):
return max(overload_arg_similarity(actual, item)
for item in formal.items)
for item in formal.relevant_items())
if isinstance(formal, TypeType):
if isinstance(actual, TypeType):
# Since Type[T] is covariant, check if actual = Type[A] is
Expand Down
2 changes: 1 addition & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def analyze_member_access(name: str,
results = [analyze_member_access(name, subtype, node, is_lvalue, is_super,
is_operator, builtin_type, not_ready_callback, msg,
original_type=original_type, chk=chk)
for subtype in typ.items]
for subtype in typ.relevant_items()]
msg.disable_type_names -= 1
return UnionType.make_simplified_union(results)
elif isinstance(typ, TupleType):
Expand Down
3 changes: 0 additions & 3 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,6 @@ class CompleteTypeVisitor(TypeQuery[bool]):
def __init__(self) -> None:
super().__init__(all)

def visit_none_type(self, t: NoneTyp) -> bool:
return experiments.STRICT_OPTIONAL

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

Expand Down
12 changes: 11 additions & 1 deletion mypy/experiments.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from typing import Optional, Tuple
from contextlib import contextmanager
from typing import Optional, Tuple, Iterator
STRICT_OPTIONAL = False
find_occurrences = None # type: Optional[Tuple[str, str]]


@contextmanager
def strict_optional_set(value: bool) -> Iterator[None]:
global STRICT_OPTIONAL
saved = STRICT_OPTIONAL
STRICT_OPTIONAL = value
yield
STRICT_OPTIONAL = saved
Copy link
Member

Choose a reason for hiding this comment

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

I know you said it was hacky, but this is where I realized quite how hacky... :-)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep. :/

8 changes: 4 additions & 4 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
return declared
if isinstance(declared, UnionType):
return UnionType.make_simplified_union([narrow_declared_type(x, narrowed)
for x in declared.items])
for x in declared.relevant_items()])
elif not is_overlapping_types(declared, narrowed, use_promotions=True):
if experiments.STRICT_OPTIONAL:
return UninhabitedType()
else:
return NoneTyp()
elif isinstance(narrowed, UnionType):
return UnionType.make_simplified_union([narrow_declared_type(declared, x)
for x in narrowed.items])
for x in narrowed.relevant_items()])
elif isinstance(narrowed, AnyType):
return narrowed
elif isinstance(declared, (Instance, TupleType)):
Expand Down Expand Up @@ -99,10 +99,10 @@ class C(A, B): ...
return t.type in s.type.mro or s.type in t.type.mro
if isinstance(t, UnionType):
return any(is_overlapping_types(item, s)
for item in t.items)
for item in t.relevant_items())
if isinstance(s, UnionType):
return any(is_overlapping_types(t, item)
for item in s.items)
for item in s.relevant_items())
if isinstance(t, TypeType) and isinstance(s, TypeType):
# If both types are TypeType, compare their inner types.
return is_overlapping_types(t.item, s.item, use_promotions)
Expand Down
3 changes: 2 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ class Options:
"ignore_errors",
"strict_boolean",
"no_implicit_optional",
"strict_optional",
}

OPTIONS_AFFECTING_CACHE = PER_MODULE_OPTIONS | {"strict_optional", "quick_and_dirty"}
OPTIONS_AFFECTING_CACHE = PER_MODULE_OPTIONS | {"quick_and_dirty"}

def __init__(self) -> None:
# -- build options --
Expand Down
Loading