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

Defer subclass methods if superclass has not been analyzed #5637

Merged
merged 9 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from 8 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
105 changes: 76 additions & 29 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from contextlib import contextmanager

from typing import (
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Iterable, Any
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator, Iterable,
Sequence
)

from mypy.errors import Errors, report_internal_error
Expand Down Expand Up @@ -72,19 +73,31 @@

DEFAULT_LAST_PASS = 1 # type: Final # Pass numbers start at 0

DeferredNodeType = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
FineDeferredNodeType = Union[FuncDef, MypyFile, OverloadedFuncDef]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style nit: I'd prefer to call this FineGrainedDeferredNodeType or something.

We don't use "fine" as a shorted version of "fine-grained" elsewhere and it can be confusing.


# A node which is postponed to be processed during the next pass.
# This is used for both batch mode and fine-grained incremental mode.
# In normal mode one can defer functions and methods (also decorated and/or overloaded)
# and lambda expressions. Nested functions can't be deferred -- only top-level functions
# and methods of classes not defined within a function can be deferred.
DeferredNode = NamedTuple(
'DeferredNode',
[
# In batch mode only FuncDef and LambdaExpr are supported
('node', Union[FuncDef, LambdaExpr, MypyFile, OverloadedFuncDef]),
('node', DeferredNodeType),
('context_type_name', Optional[str]), # Name of the surrounding class (for error messages)
('active_typeinfo', Optional[TypeInfo]), # And its TypeInfo (for semantic analysis
# self type handling)
])

# Same as above, but for fine-grained mode targets. Only top-level functions/methods
# and module top levels are allowed as such.
FineDeferredNode = NamedTuple(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to above, I'd prefer to name this FineGrainedDeferredNode.

'FineDeferredNode',
[
('node', FineDeferredNodeType),
('context_type_name', Optional[str]),
('active_typeinfo', Optional[TypeInfo]),
])

# Data structure returned by find_isinstance_check representing
# information learned from the truth or falsehood of a condition. The
Expand Down Expand Up @@ -283,7 +296,8 @@ def check_first_pass(self) -> None:

self.tscope.leave()

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

This goes through deferred nodes, returning True if there were any.
Expand All @@ -300,7 +314,7 @@ def check_second_pass(self, todo: Optional[List[DeferredNode]] = None) -> bool:
else:
assert not self.deferred_nodes
self.deferred_nodes = []
done = set() # type: Set[Union[FuncDef, LambdaExpr, MypyFile, OverloadedFuncDef]]
done = set() # type: Set[Union[DeferredNodeType, FineDeferredNodeType]]
for node, type_name, active_typeinfo in todo:
if node in done:
continue
Expand All @@ -314,10 +328,7 @@ def check_second_pass(self, todo: Optional[List[DeferredNode]] = None) -> bool:
self.tscope.leave()
return True

def check_partial(self, node: Union[FuncDef,
LambdaExpr,
MypyFile,
OverloadedFuncDef]) -> None:
def check_partial(self, node: Union[DeferredNodeType, FineDeferredNodeType]) -> None:
if isinstance(node, MypyFile):
self.check_top_level(node)
else:
Expand All @@ -338,20 +349,32 @@ def check_top_level(self, node: MypyFile) -> None:
assert not self.current_node_deferred
# TODO: Handle __all__

def defer_node(self, node: DeferredNodeType, enclosing_class: Optional[TypeInfo]) -> None:
ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
"""Defer a node for processing during next type-checking pass.

Args:
node: function/method being deferred
enclosing_class: for methods, the class where the method is defined
NOTE: this can't handle nested functions/methods.
"""
if self.errors.type_name:
type_name = self.errors.type_name[-1]
else:
type_name = None
# We don't freeze the entire scope since only top-level functions and methods
# can be deferred. Only module/class level scope information is needed.
# Module-level scope information is preserved in the TypeChecker instance.
self.deferred_nodes.append(DeferredNode(node, type_name, enclosing_class))

def handle_cannot_determine_type(self, name: str, context: Context) -> None:
node = self.scope.top_non_lambda_function()
if self.pass_num < self.last_pass and isinstance(node, FuncDef):
# Don't report an error yet. Just defer. Note that we don't defer
# lambdas because they are coupled to the surrounding function
# through the binder and the inferred type of the lambda, so it
# would get messy.
if self.errors.type_name:
type_name = self.errors.type_name[-1]
else:
type_name = None
# Shouldn't we freeze the entire scope?
enclosing_class = self.scope.enclosing_class()
self.deferred_nodes.append(DeferredNode(node, type_name, enclosing_class))
self.defer_node(node, enclosing_class)
# Set a marker so that we won't infer additional types in this
# function. Any inferred types could be bogus, because there's at
# least one type that we don't know.
Expand Down Expand Up @@ -1253,15 +1276,26 @@ def expand_typevars(self, defn: FuncItem,
else:
return [(defn, typ)]

def check_method_override(self, defn: Union[FuncBase, Decorator]) -> None:
"""Check if function definition is compatible with base classes."""
def check_method_override(self, defn: Union[FuncDef, OverloadedFuncDef, Decorator]) -> None:
"""Check if function definition is compatible with base classes.

This may defer the method if a signature is not available in at least on base class.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Typo: "on" -> "one"

"""
# Check against definitions in base classes.
for base in defn.info.mro[1:]:
self.check_method_or_accessor_override_for_base(defn, base)
if self.check_method_or_accessor_override_for_base(defn, base):
# Node was deferred, we will have another attempt later.
return

def check_method_or_accessor_override_for_base(self, defn: Union[FuncDef,
OverloadedFuncDef,
Decorator],
base: TypeInfo) -> bool:
"""Check if method definition is compatible with a base class.

def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decorator],
base: TypeInfo) -> None:
"""Check if method definition is compatible with a base class."""
Return True if the node was deferred because one of the corresponding
superclass nodes is not ready.
"""
if base:
name = defn.name()
base_attr = base.names.get(name)
Expand All @@ -1277,19 +1311,26 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decor
if name not in ('__init__', '__new__', '__init_subclass__'):
# Check method override
# (__init__, __new__, __init_subclass__ are special).
self.check_method_override_for_base_with_name(defn, name, base)
if self.check_method_override_for_base_with_name(defn, name, base):
return True
if name in nodes.inplace_operator_methods:
# Figure out the name of the corresponding operator method.
method = '__' + name[3:]
# An inplace operator method such as __iadd__ might not be
# always introduced safely if a base class defined __add__.
# TODO can't come up with an example where this is
# necessary; now it's "just in case"
self.check_method_override_for_base_with_name(defn, method,
base)
return self.check_method_override_for_base_with_name(defn, method,
base)
return False

def check_method_override_for_base_with_name(
self, defn: Union[FuncBase, Decorator], name: str, base: TypeInfo) -> None:
self, defn: Union[FuncDef, OverloadedFuncDef, Decorator],
name: str, base: TypeInfo) -> bool:
"""Check if overriding an attribute `name` of `base` with `defn` is valid.

Return True if the supertype node was not analysed yet, and `defn` was deferred.
"""
base_attr = base.names.get(name)
if base_attr:
# The name of the method is defined in the base class.
Expand All @@ -1302,7 +1343,7 @@ def check_method_override_for_base_with_name(
context = defn.func

# Construct the type of the overriding method.
if isinstance(defn, FuncBase):
if isinstance(defn, (FuncDef, OverloadedFuncDef)):
typ = self.function_type(defn) # type: Type
override_class_or_static = defn.is_class or defn.is_static
else:
Expand All @@ -1317,13 +1358,18 @@ def check_method_override_for_base_with_name(
original_type = base_attr.type
original_node = base_attr.node
if original_type is None:
if isinstance(original_node, FuncBase):
if self.pass_num < self.last_pass:
ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
# If there are passes left, defer this node until next pass,
# otherwise try reconstructing the method type from available information.
self.defer_node(defn, defn.info)
return True
elif isinstance(original_node, (FuncDef, OverloadedFuncDef)):
original_type = self.function_type(original_node)
elif isinstance(original_node, Decorator):
original_type = self.function_type(original_node.func)
else:
assert False, str(base_attr.node)
if isinstance(original_node, FuncBase):
if isinstance(original_node, (FuncDef, OverloadedFuncDef)):
original_class_or_static = original_node.is_class or original_node.is_static
elif isinstance(original_node, Decorator):
fdef = original_node.func
Expand Down Expand Up @@ -1359,6 +1405,7 @@ def check_method_override_for_base_with_name(
else:
self.msg.signature_incompatible_with_supertype(
defn.name(), name, base.name(), context)
return False

def get_op_other_domain(self, tp: FunctionLike) -> Optional[Type]:
if isinstance(tp, CallableType):
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
del self.cur_mod_node
del self.globals

def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
def refresh_partial(self, node: Union[MypyFile, FuncDef, OverloadedFuncDef],
patches: List[Tuple[int, Callable[[], None]]]) -> None:
"""Refresh a stale target in fine-grained incremental mode."""
self.patches = patches
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal_pass3.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
del self.cur_mod_node
self.patches = []

def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
def refresh_partial(self, node: Union[MypyFile, FuncDef, OverloadedFuncDef],
patches: List[Tuple[int, Callable[[], None]]]) -> None:
"""Refresh a stale target in fine-grained incremental mode."""
self.options = self.sem.options
Expand Down
2 changes: 1 addition & 1 deletion mypy/server/aststrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from mypy.typestate import TypeState


def strip_target(node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None:
def strip_target(node: Union[MypyFile, FuncDef, OverloadedFuncDef]) -> None:
"""Reset a fine-grained incremental target to state after semantic analysis pass 1.

NOTE: Currently we opportunistically only reset changes that are known to otherwise
Expand Down
24 changes: 11 additions & 13 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
BuildManager, State, BuildSource, BuildResult, Graph, load_graph,
process_fresh_modules, DEBUG_FINE_GRAINED,
)
from mypy.checker import DeferredNode
from mypy.checker import FineDeferredNode
from mypy.errors import CompileError
from mypy.nodes import (
MypyFile, FuncDef, TypeInfo, SymbolNode, Decorator,
Expand Down Expand Up @@ -780,15 +780,15 @@ def find_targets_recursive(
graph: Graph,
triggers: Set[str],
deps: Dict[str, Set[str]],
up_to_date_modules: Set[str]) -> Tuple[Dict[str, Set[DeferredNode]],
up_to_date_modules: Set[str]) -> Tuple[Dict[str, Set[FineDeferredNode]],
Set[str], Set[TypeInfo]]:
"""Find names of all targets that need to reprocessed, given some triggers.

Returns: A tuple containing a:
* Dictionary from module id to a set of stale targets.
* A set of module ids for unparsed modules with stale targets.
"""
result = {} # type: Dict[str, Set[DeferredNode]]
result = {} # type: Dict[str, Set[FineDeferredNode]]
worklist = triggers
processed = set() # type: Set[str]
stale_protos = set() # type: Set[TypeInfo]
Expand Down Expand Up @@ -834,7 +834,7 @@ def find_targets_recursive(
def reprocess_nodes(manager: BuildManager,
graph: Dict[str, State],
module_id: str,
nodeset: Set[DeferredNode],
nodeset: Set[FineDeferredNode],
deps: Dict[str, Set[str]]) -> Set[str]:
"""Reprocess a set of nodes within a single module.

Expand All @@ -850,7 +850,7 @@ def reprocess_nodes(manager: BuildManager,
old_symbols = {name: names.copy() for name, names in old_symbols.items()}
old_symbols_snapshot = snapshot_symbol_table(file_node.fullname(), file_node.names)

def key(node: DeferredNode) -> int:
def key(node: FineDeferredNode) -> int:
# Unlike modules which are sorted by name within SCC,
# nodes within the same module are sorted by line number, because
# this is how they are processed in normal mode.
Expand Down Expand Up @@ -959,7 +959,7 @@ def find_symbol_tables_recursive(prefix: str, symbols: SymbolTable) -> Dict[str,


def update_deps(module_id: str,
nodes: List[DeferredNode],
nodes: List[FineDeferredNode],
graph: Dict[str, State],
deps: Dict[str, Set[str]],
options: Options) -> None:
Expand All @@ -977,7 +977,7 @@ def update_deps(module_id: str,


def lookup_target(manager: BuildManager,
target: str) -> Tuple[List[DeferredNode], Optional[TypeInfo]]:
target: str) -> Tuple[List[FineDeferredNode], Optional[TypeInfo]]:
"""Look up a target by fully-qualified name.

The first item in the return tuple is a list of deferred nodes that
Expand Down Expand Up @@ -1025,7 +1025,7 @@ def not_found() -> None:
# a deserialized TypeInfo with missing attributes.
not_found()
return [], None
result = [DeferredNode(file, None, None)]
result = [FineDeferredNode(file, None, None)]
stale_info = None # type: Optional[TypeInfo]
if node.is_protocol:
stale_info = node
Expand All @@ -1050,15 +1050,15 @@ def not_found() -> None:
# context will be wrong and it could be a partially initialized deserialized node.
not_found()
return [], None
return [DeferredNode(node, active_class_name, active_class)], None
return [FineDeferredNode(node, active_class_name, active_class)], None


def is_verbose(manager: BuildManager) -> bool:
return manager.options.verbosity >= 1 or DEBUG_FINE_GRAINED


def target_from_node(module: str,
node: Union[FuncDef, MypyFile, OverloadedFuncDef, LambdaExpr]
node: Union[FuncDef, MypyFile, OverloadedFuncDef]
) -> Optional[str]:
"""Return the target name corresponding to a deferred node.

Expand All @@ -1073,10 +1073,8 @@ def target_from_node(module: str,
# Actually a reference to another module -- likely a stale dependency.
return None
return module
elif isinstance(node, (OverloadedFuncDef, FuncDef)):
else: # OverloadedFuncDef or FuncDef
if node.info:
return '%s.%s' % (node.info.fullname(), node.name())
else:
return '%s.%s' % (module, node.name())
else:
assert False, "Lambda expressions can't be deferred in fine-grained incremental mode"
Loading