Skip to content

Various fixes to the fine-grained incremental mode #4438

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 16 commits into from
Jan 10, 2018
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
85 changes: 60 additions & 25 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,20 @@ class Errors:
# Set to True to show column numbers in error messages.
show_column_numbers = False # type: bool

# Stack of active fine-grained incremental checking targets within
# a module. The first item is always the current module id.
# State for keeping track of the current fine-grained incremental mode target.
Copy link
Collaborator

@msullivan msullivan Jan 9, 2018

Choose a reason for hiding this comment

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

Is Errors the best place to be tracking what target is currently being processed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really. Errors is tracking this primarily for convenience -- it's the only place that currently has enough context and is available pretty much everywhere. A better approach might be to have a separate context class which would track the current scope and target, and Errors would get access to this context (but it would only need read-only access). Since this PR doesn't change the responsibilities of Errors significantly (the wart was already there before), I'd prefer to refactor this in a separate PR.

# (See mypy.server.update for more about targets.)
target = None # type: List[str]
#
# Current module id.
target_module = None # type: Optional[str]
# Partially qualified name of target class; without module prefix (examples: 'C' is top-level,
# 'C.D' nested).
target_class = None # type: Optional[str]
# Short name of the outermost function/method.
target_function = None # type: Optional[str]
# Nesting depth of functions/classes within the outermost function/method. These aren't
# separate targets and they are included in the surrounding function, but we this counter
# for internal bookkeeping.
target_ignore_depth = 0

def __init__(self, show_error_context: bool = False,
show_column_numbers: bool = False) -> None:
Expand All @@ -150,7 +160,10 @@ def initialize(self) -> None:
self.used_ignored_lines = defaultdict(set)
self.ignored_files = set()
self.only_once_messages = set()
self.target = []
self.target_module = None
self.target_class = None
self.target_function = None
self.target_ignore_depth = 0

def reset(self) -> None:
self.initialize()
Expand All @@ -161,7 +174,10 @@ def copy(self) -> 'Errors':
new.import_ctx = self.import_ctx[:]
new.type_name = self.type_name[:]
new.function_or_member = self.function_or_member[:]
new.target = self.target[:]
new.target_module = self.target_module
new.target_class = self.target_class
new.target_function = self.target_function
new.target_ignore_depth = self.target_ignore_depth
return new

def set_ignore_prefix(self, prefix: str) -> None:
Expand All @@ -186,8 +202,10 @@ def set_file(self, file: str,
# reporting errors for files other than the one currently being
# processed.
self.file = file
if module:
self.target = [module]
self.target_module = module
self.target_class = None
self.target_function = None
self.target_ignore_depth = 0

def set_file_ignored_lines(self, file: str,
ignored_lines: Set[int],
Expand All @@ -198,12 +216,18 @@ def set_file_ignored_lines(self, file: str,

def push_function(self, name: str) -> None:
"""Set the current function or member short name (it can be None)."""
self.push_target_component(name)
if self.target_function is None:
self.target_function = name
else:
self.target_ignore_depth += 1
self.function_or_member.append(name)

def pop_function(self) -> None:
self.function_or_member.pop()
self.pop_target_component()
if self.target_ignore_depth > 0:
self.target_ignore_depth -= 1
else:
self.target_function = None

@contextmanager
def enter_function(self, name: str) -> Iterator[None]:
Expand All @@ -213,30 +237,41 @@ def enter_function(self, name: str) -> Iterator[None]:

def push_type(self, name: str) -> None:
"""Set the short name of the current type (it can be None)."""
self.push_target_component(name)
if self.target_function is not None:
self.target_ignore_depth += 1
elif self.target_class is None:
self.target_class = name
else:
self.target_class += '.' + name
self.type_name.append(name)

def pop_type(self) -> None:
self.type_name.pop()
self.pop_target_component()

def push_target_component(self, name: str) -> None:
if self.target and not self.function_or_member[-1]:
self.target.append('{}.{}'.format(self.target[-1], name))

def pop_target_component(self) -> None:
if self.target and not self.function_or_member[-1]:
self.target.pop()
if self.target_ignore_depth > 0:
self.target_ignore_depth -= 1
else:
assert self.target_class is not None
if '.' in self.target_class:
self.target_class = '.'.join(self.target_class.split('.')[:-1])
else:
self.target_class = None

def current_target(self) -> Optional[str]:
if self.target:
return self.target[-1]
return None
if self.target_module is None:
return None
target = self.target_module
if self.target_function is not None:
# Only include class name if we are inside a method, since a class
# target also includes all methods, which is not what we want
# here. Instead, the current target for a class body is the
# enclosing module top level.
if self.target_class is not None:
target += '.' + self.target_class
target += '.' + self.target_function
return target

def current_module(self) -> Optional[str]:
if self.target:
return self.target[0]
return None
return self.target_module

@contextmanager
def enter_type(self, name: str) -> Iterator[None]:
Expand Down
4 changes: 3 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class FuncBase(Node):
# Original, not semantically analyzed type (used for reprocessing)
unanalyzed_type = None # type: Optional[mypy.types.Type]
# If method, reference to TypeInfo
# TODO: The type should be Optional[TypeInfo]
info = None # type: TypeInfo
is_property = False
_fullname = None # type: str # Name with module prefix
Expand Down Expand Up @@ -597,6 +598,7 @@ class Var(SymbolNode):

_name = None # type: str # Name without module prefix
_fullname = None # type: str # Name with module prefix
# TODO: The following should be Optional[TypeInfo]
info = None # type: TypeInfo # Defining class (for member variables)
type = None # type: Optional[mypy.types.Type] # Declared or inferred type, or None
# Is this the first argument to an ordinary method (usually "self")?
Expand Down Expand Up @@ -1468,7 +1470,7 @@ class SuperExpr(Expression):
"""Expression super().name"""

name = ''
info = None # type: TypeInfo # Type that contains this super expression
info = None # type: Optional[TypeInfo] # Type that contains this super expression
call = None # type: CallExpr # The expression super(...)

def __init__(self, name: str, call: CallExpr) -> None:
Expand Down
10 changes: 6 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,6 @@ class SemanticAnalyzerPass2(NodeVisitor[None], SemanticAnalyzerPluginInterface):
type = None # type: Optional[TypeInfo]
# Stack of outer classes (the second tuple item contains tvars).
type_stack = None # type: List[Optional[TypeInfo]]
# Type variables that are bound by the directly enclosing class
bound_tvars = None # type: List[SymbolTableNode]
# Type variables bound by the current scope, be it class or function
tvar_scope = None # type: TypeVarScope
# Per-module options
Expand Down Expand Up @@ -332,9 +330,11 @@ def file_context(self, file_node: MypyFile, fnam: str, options: Options,
self.is_stub_file = fnam.lower().endswith('.pyi')
self.is_typeshed_stub_file = self.errors.is_typeshed_file(file_node.path)
self.globals = file_node.names
self.tvar_scope = TypeVarScope()
if active_type:
self.enter_class(active_type.defn.info)
# TODO: Bind class type vars
for tvar in active_type.defn.type_vars:
self.tvar_scope.bind_existing(tvar)

yield

Expand Down Expand Up @@ -909,7 +909,8 @@ def clean_up_bases_and_infer_type_variables(self, defn: ClassDef) -> None:
del defn.base_type_exprs[i]
tvar_defs = [] # type: List[TypeVarDef]
for name, tvar_expr in declared_tvars:
tvar_defs.append(self.tvar_scope.bind(name, tvar_expr))
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
tvar_defs.append(tvar_def)
defn.type_vars = tvar_defs

def analyze_typevar_declaration(self, t: Type) -> Optional[TypeVarList]:
Expand Down Expand Up @@ -2844,6 +2845,7 @@ def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -
var = Var(item)
var.info = info
var.is_property = True
var._fullname = '{}.{}'.format(self.qualified_name(name), item)
info.names[item] = SymbolTableNode(MDEF, var)
return info

Expand Down
62 changes: 58 additions & 4 deletions mypy/server/astmerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@

from mypy.nodes import (
Node, MypyFile, SymbolTable, Block, AssignmentStmt, NameExpr, MemberExpr, RefExpr, TypeInfo,
FuncDef, ClassDef, NamedTupleExpr, SymbolNode, Var, Statement, SuperExpr, MDEF
FuncDef, ClassDef, NamedTupleExpr, SymbolNode, Var, Statement, SuperExpr, NewTypeExpr,
OverloadedFuncDef, LambdaExpr, TypedDictExpr, EnumCallExpr, MDEF
)
from mypy.traverser import TraverserVisitor
from mypy.types import (
Type, TypeVisitor, Instance, AnyType, NoneTyp, CallableType, DeletedType, PartialType,
TupleType, TypeType, TypeVarType, TypedDictType, UnboundType, UninhabitedType, UnionType,
Overloaded
Overloaded, TypeVarDef
)
from mypy.util import get_prefix

Expand Down Expand Up @@ -157,14 +158,29 @@ def visit_func_def(self, node: FuncDef) -> None:
node = self.fixup(node)
if node.type:
self.fixup_type(node.type)
if node.info:
node.info = self.fixup(node.info)
super().visit_func_def(node)

def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> None:
if node.info:
node.info = self.fixup(node.info)
super().visit_overloaded_func_def(node)

def visit_class_def(self, node: ClassDef) -> None:
# TODO additional things like the MRO
# TODO additional things?
node.defs.body = self.replace_statements(node.defs.body)
node.info = self.fixup(node.info)
for tv in node.type_vars:
self.process_type_var_def(tv)
self.process_type_info(node.info)
super().visit_class_def(node)

def process_type_var_def(self, tv: TypeVarDef) -> None:
for value in tv.values:
self.fixup_type(value)
self.fixup_type(tv.upper_bound)

def visit_assignment_stmt(self, node: AssignmentStmt) -> None:
if node.type:
self.fixup_type(node.type)
Expand All @@ -191,7 +207,39 @@ def visit_namedtuple_expr(self, node: NamedTupleExpr) -> None:

def visit_super_expr(self, node: SuperExpr) -> None:
super().visit_super_expr(node)
if node.info is not None:
node.info = self.fixup(node.info)

def visit_newtype_expr(self, node: NewTypeExpr) -> None:
if node.info:
node.info = self.fixup(node.info)
self.process_type_info(node.info)
if node.old_type:
self.fixup_type(node.old_type)
super().visit_newtype_expr(node)

def visit_lambda_expr(self, node: LambdaExpr) -> None:
if node.info:
node.info = self.fixup(node.info)
super().visit_lambda_expr(node)

def visit_typeddict_expr(self, node: TypedDictExpr) -> None:
node.info = self.fixup(node.info)
super().visit_typeddict_expr(node)

def visit_enum_call_expr(self, node: EnumCallExpr) -> None:
node.info = self.fixup(node.info)
self.process_type_info(node.info)
super().visit_enum_call_expr(node)

# Others

def visit_var(self, node: Var) -> None:
if node.info:
node.info = self.fixup(node.info)
if node.type:
self.fixup_type(node.type)
super().visit_var(node)

# Helpers

Expand All @@ -206,7 +254,13 @@ def fixup_type(self, typ: Type) -> None:
typ.accept(TypeReplaceVisitor(self.replacements))

def process_type_info(self, info: TypeInfo) -> None:
# TODO additional things like the MRO
# TODO: Additional things:
# - declared_metaclass
# - metaclass_type
# - _promote
# - tuple_type
# - typeddict_type
# - replaced
replace_nodes_in_symbol_table(info.names, self.replacements)
for i, item in enumerate(info.mro):
info.mro[i] = self.fixup(info.mro[i])
Expand Down
35 changes: 19 additions & 16 deletions mypy/server/aststrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from mypy.nodes import (
Node, FuncDef, NameExpr, MemberExpr, RefExpr, MypyFile, FuncItem, ClassDef, AssignmentStmt,
ImportFrom, Import, TypeInfo, SymbolTable, Var, CallExpr, Decorator, OverloadedFuncDef,
UNBOUND_IMPORTED, GDEF
SuperExpr, UNBOUND_IMPORTED, GDEF, MDEF
)
from mypy.traverser import TraverserVisitor

Expand Down Expand Up @@ -83,6 +83,10 @@ def visit_class_def(self, node: ClassDef) -> None:
node.info.abstract_attributes = []
node.info.mro = []
node.info.add_type_vars()
node.info.tuple_type = None
node.info.typeddict_type = None
node.info._cache = set()
node.info._cache_proper = set()
node.base_type_exprs.extend(node.removed_base_type_exprs)
node.removed_base_type_exprs = []
with self.enter_class(node.info):
Expand Down Expand Up @@ -136,20 +140,8 @@ def enter_method(self, info: TypeInfo) -> Iterator[None]:

def visit_assignment_stmt(self, node: AssignmentStmt) -> None:
node.type = node.unanalyzed_type
if node.type and self.is_class_body:
# Remove attribute defined in the class body from the class namespace to avoid
# bogus "Name already defined" errors.
#
# TODO: Handle multiple assignment, other lvalues
# TODO: What about assignments without type annotations?
assert len(node.lvalues) == 1
lvalue = node.lvalues[0]
assert isinstance(lvalue, NameExpr)
assert self.type is not None # Because self.is_class_body is True
del self.type.names[lvalue.name]
if self.type and not self.is_class_body:
# TODO: Handle multiple assignment
# TODO: Merge with above
if len(node.lvalues) == 1:
lvalue = node.lvalues[0]
if isinstance(lvalue, MemberExpr) and lvalue.is_new_def:
Expand Down Expand Up @@ -192,6 +184,9 @@ def visit_name_expr(self, node: NameExpr) -> None:
# Global assignments are processed in semantic analysis pass 1, and we
# only want to strip changes made in passes 2 or later.
if not (node.kind == GDEF and node.is_new_def):
# Remove defined attributes so that they can recreated during semantic analysis.
if node.kind == MDEF and node.is_new_def:
self.strip_class_attr(node.name)
self.strip_ref_expr(node)

def visit_member_expr(self, node: MemberExpr) -> None:
Expand All @@ -205,12 +200,14 @@ def visit_member_expr(self, node: MemberExpr) -> None:
# defines an attribute with the same name, and we can't have
# multiple definitions for an attribute. Defer to the base class
# definition.
if self.type is not None:
del self.type.names[node.name]
node.is_inferred_def = False
self.strip_class_attr(node.name)
node.def_var = None
super().visit_member_expr(node)

def strip_class_attr(self, name: str) -> None:
if self.type is not None:
del self.type.names[name]

def is_duplicate_attribute_def(self, node: MemberExpr) -> bool:
if not node.is_inferred_def:
return False
Expand All @@ -223,11 +220,17 @@ def strip_ref_expr(self, node: RefExpr) -> None:
node.kind = None
node.node = None
node.fullname = None
node.is_new_def = False
node.is_inferred_def = False

def visit_call_expr(self, node: CallExpr) -> None:
node.analyzed = None
super().visit_call_expr(node)

def visit_super_expr(self, node: SuperExpr) -> None:
node.info = None
super().visit_super_expr(node)

# TODO: handle more node types


Expand Down
Loading