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 1 commit
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
3 changes: 2 additions & 1 deletion mypy/server/astmerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ def visit_func_def(self, node: FuncDef) -> None:
super().visit_func_def(node)

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

Expand Down
2 changes: 1 addition & 1 deletion mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def update_single(self, module: str, path: str) -> Tuple[List[str],
filtered = [trigger for trigger in triggered
if not trigger.endswith('__>')]
print('triggered:', sorted(filtered))
self.triggered.extend(triggered)
self.triggered.extend(triggered | self.previous_targets_with_errors)
update_dependencies({module: tree}, self.deps, graph, self.options)
propagate_changes_using_dependencies(manager, graph, self.deps, triggered,
{module},
Expand Down
39 changes: 34 additions & 5 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -1025,8 +1025,8 @@ def f() -> Iterator[None]:
[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]
[triggered]
2: <b>
3: <b>
2: <b>, __main__
3: <b>, __main__, a
[out]
main:2: error: Revealed type is 'contextlib.GeneratorContextManager[builtins.None]'
==
Expand Down Expand Up @@ -1072,9 +1072,9 @@ def g() -> None:
[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]
[triggered]
2: <b.h>, <b>
3: <b.h>, <b>
4:
2: <b.h>, <b>, a.g
3: <b.h>, <b>, a
4: a.g
[out]
a.py:11: error: Too many arguments for "h"
==
Expand Down Expand Up @@ -1346,3 +1346,32 @@ def g() -> str: pass
[builtins fixtures/exception.pyi]
[out]
==

[case testMroSpecialCase]
import b
import a

[file a.py]
class C: pass
class D(C):
1()
class E(D): pass

[file b.py]
import a

[file a.py.2]
class C: pass
class D(C):
1()
class E(D): pass

[file b.py.2]
import a

[triggered]
2: a, a
[out]
a.py:3: error: "int" not callable
==
a.py:3: error: "int" not callable