Skip to content

Commit e037806

Browse files
JukkaLgvanrossum
authored andcommitted
More improvements to fine-grained incremental mode (#4274)
This contains several improvements to fine-grained incremental mode, including these: Support for blocking errors. Adding a (single) file. Removing a (single) file. Share the saved cache between dmypy and fine-grained incremental mode. Support re-creating a previously deleted file in test cases. This cuts some corners for now. The main limitation is that only changes involving a single file are supported.
1 parent 0bbf714 commit e037806

File tree

11 files changed

+842
-149
lines changed

11 files changed

+842
-149
lines changed

mypy/build.py

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def is_source(self, file: MypyFile) -> bool:
119119

120120
# A dict containing saved cache data from a previous run. This will
121121
# be updated in place with newly computed cache data. See dmypy.py.
122-
SavedCache = Dict[str, Tuple['CacheMeta', MypyFile]]
122+
SavedCache = Dict[str, Tuple['CacheMeta', MypyFile, Dict[Expression, Type]]]
123123

124124

125125
def build(sources: List[BuildSource],
@@ -335,6 +335,7 @@ def default_lib_path(data_dir: str,
335335
CacheMeta = NamedTuple('CacheMeta',
336336
[('id', str),
337337
('path', str),
338+
('memory_only', bool), # no corresponding json files (fine-grained only)
338339
('mtime', int),
339340
('size', int),
340341
('hash', str),
@@ -359,6 +360,7 @@ def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
359360
return CacheMeta(
360361
meta.get('id', sentinel),
361362
meta.get('path', sentinel),
363+
meta.get('memory_only', False),
362364
int(meta['mtime']) if 'mtime' in meta else sentinel,
363365
meta.get('size', sentinel),
364366
meta.get('hash', sentinel),
@@ -510,7 +512,8 @@ class BuildManager:
510512
version_id: The current mypy version (based on commit id when possible)
511513
plugin: Active mypy plugin(s)
512514
errors: Used for reporting all errors
513-
saved_cache: Dict with saved cache state for dmypy (read-write!)
515+
saved_cache: Dict with saved cache state for dmypy and fine-grained incremental mode
516+
(read-write!)
514517
stats: Dict with various instrumentation numbers
515518
"""
516519

@@ -642,19 +645,20 @@ def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> My
642645
self.errors.set_file_ignored_lines(path, tree.ignored_lines, ignore_errors)
643646
return tree
644647

645-
def module_not_found(self, path: str, line: int, id: str) -> None:
648+
def module_not_found(self, path: str, id: str, line: int, target: str) -> None:
646649
self.errors.set_file(path, id)
647650
stub_msg = "(Stub files are from https://github.com/python/typeshed)"
648-
if ((self.options.python_version[0] == 2 and moduleinfo.is_py2_std_lib_module(id)) or
649-
(self.options.python_version[0] >= 3 and moduleinfo.is_py3_std_lib_module(id))):
651+
if ((self.options.python_version[0] == 2 and moduleinfo.is_py2_std_lib_module(target)) or
652+
(self.options.python_version[0] >= 3 and
653+
moduleinfo.is_py3_std_lib_module(target))):
650654
self.errors.report(
651-
line, 0, "No library stub file for standard library module '{}'".format(id))
655+
line, 0, "No library stub file for standard library module '{}'".format(target))
652656
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
653-
elif moduleinfo.is_third_party_module(id):
654-
self.errors.report(line, 0, "No library stub file for module '{}'".format(id))
657+
elif moduleinfo.is_third_party_module(target):
658+
self.errors.report(line, 0, "No library stub file for module '{}'".format(target))
655659
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
656660
else:
657-
self.errors.report(line, 0, "Cannot find module named '{}'".format(id))
661+
self.errors.report(line, 0, "Cannot find module named '{}'".format(target))
658662
self.errors.report(line, 0, '(Perhaps setting MYPYPATH '
659663
'or using the "--ignore-missing-imports" flag would help)',
660664
severity='note', only_once=True)
@@ -937,7 +941,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
937941
"""
938942
saved_cache = manager.saved_cache
939943
if id in saved_cache:
940-
m, t = saved_cache[id]
944+
m, t, types = saved_cache[id]
941945
manager.add_stats(reused_metas=1)
942946
manager.trace("Reusing saved metadata for %s" % id)
943947
# Note: it could still be skipped if the mtime/size/hash mismatches.
@@ -1036,6 +1040,12 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
10361040
manager.log('Metadata abandoned for {}: errors were previously ignored'.format(id))
10371041
return None
10381042

1043+
if meta.memory_only:
1044+
# Special case for fine-grained incremental mode when the JSON file is missing but
1045+
# we want to cache the module anyway.
1046+
manager.log('Memory-only metadata for {}'.format(id))
1047+
return meta
1048+
10391049
assert path is not None, "Internal error: meta was provided without a path"
10401050
# Check data_json; assume if its mtime matches it's good.
10411051
# TODO: stat() errors
@@ -1441,6 +1451,10 @@ class State:
14411451
# Whether to ignore all errors
14421452
ignore_all = False
14431453

1454+
# Type checker used for checking this file. Use type_checker() for
1455+
# access and to construct this on demand.
1456+
_type_checker = None # type: Optional[TypeChecker]
1457+
14441458
def __init__(self,
14451459
id: Optional[str],
14461460
path: Optional[str],
@@ -1464,6 +1478,7 @@ def __init__(self,
14641478
self.import_context = []
14651479
self.id = id or '__main__'
14661480
self.options = manager.options.clone_for_module(self.id)
1481+
self._type_checker = None
14671482
if not path and source is None:
14681483
assert id is not None
14691484
file_id = id
@@ -1511,7 +1526,8 @@ def __init__(self,
15111526
if not self.options.ignore_missing_imports:
15121527
save_import_context = manager.errors.import_context()
15131528
manager.errors.set_import_context(caller_state.import_context)
1514-
manager.module_not_found(caller_state.xpath, caller_line, id)
1529+
manager.module_not_found(caller_state.xpath, caller_state.id,
1530+
caller_line, id)
15151531
manager.errors.set_import_context(save_import_context)
15161532
manager.missing_modules.add(id)
15171533
raise ModuleNotFound
@@ -1828,20 +1844,27 @@ def semantic_analysis_apply_patches(self) -> None:
18281844
patch_func()
18291845

18301846
def type_check_first_pass(self) -> None:
1831-
assert self.tree is not None, "Internal error: method must be called on parsed file only"
1832-
manager = self.manager
18331847
if self.options.semantic_analysis_only:
18341848
return
18351849
with self.wrap_context():
1836-
self.type_checker = TypeChecker(manager.errors, manager.modules, self.options,
1837-
self.tree, self.xpath, manager.plugin)
1838-
self.type_checker.check_first_pass()
1850+
self.type_checker().check_first_pass()
1851+
1852+
def type_checker(self) -> TypeChecker:
1853+
if not self._type_checker:
1854+
assert self.tree is not None, "Internal error: must be called on parsed file only"
1855+
manager = self.manager
1856+
self._type_checker = TypeChecker(manager.errors, manager.modules, self.options,
1857+
self.tree, self.xpath, manager.plugin)
1858+
return self._type_checker
1859+
1860+
def type_map(self) -> Dict[Expression, Type]:
1861+
return self.type_checker().type_map
18391862

18401863
def type_check_second_pass(self) -> bool:
18411864
if self.options.semantic_analysis_only:
18421865
return False
18431866
with self.wrap_context():
1844-
return self.type_checker.check_second_pass()
1867+
return self.type_checker().check_second_pass()
18451868

18461869
def finish_passes(self) -> None:
18471870
assert self.tree is not None, "Internal error: method must be called on parsed file only"
@@ -1851,16 +1874,16 @@ def finish_passes(self) -> None:
18511874
with self.wrap_context():
18521875
# Some tests want to look at the set of all types.
18531876
if manager.options.use_builtins_fixtures or manager.options.dump_deps:
1854-
manager.all_types.update(self.type_checker.type_map)
1877+
manager.all_types.update(self.type_map())
18551878

18561879
if self.options.incremental:
1857-
self._patch_indirect_dependencies(self.type_checker.module_refs,
1858-
self.type_checker.type_map)
1880+
self._patch_indirect_dependencies(self.type_checker().module_refs,
1881+
self.type_map())
18591882

18601883
if self.options.dump_inference_stats:
18611884
dump_type_stats(self.tree, self.xpath, inferred=True,
1862-
typemap=self.type_checker.type_map)
1863-
manager.report_file(self.tree, self.type_checker.type_map, self.options)
1885+
typemap=self.type_map())
1886+
manager.report_file(self.tree, self.type_map(), self.options)
18641887

18651888
def _patch_indirect_dependencies(self,
18661889
module_refs: Set[str],
@@ -1904,7 +1927,7 @@ def write_cache(self) -> None:
19041927
self.meta = None
19051928
self.mark_interface_stale(on_errors=True)
19061929
return
1907-
dep_prios = [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
1930+
dep_prios = self.dependency_priorities()
19081931
new_interface_hash, self.meta = write_cache(
19091932
self.id, self.path, self.tree,
19101933
list(self.dependencies), list(self.suppressed), list(self.child_modules),
@@ -1917,6 +1940,9 @@ def write_cache(self) -> None:
19171940
self.mark_interface_stale()
19181941
self.interface_hash = new_interface_hash
19191942

1943+
def dependency_priorities(self) -> List[int]:
1944+
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
1945+
19201946

19211947
def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
19221948
set_orig = set(manager.saved_cache)
@@ -1963,7 +1989,7 @@ def preserve_cache(graph: Graph) -> SavedCache:
19631989
for id, state in graph.items():
19641990
assert state.id == id
19651991
if state.meta is not None and state.tree is not None:
1966-
saved_cache[id] = (state.meta, state.tree)
1992+
saved_cache[id] = (state.meta, state.tree, state.type_map())
19671993
return saved_cache
19681994

19691995

mypy/server/aststrip.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from mypy.nodes import (
1010
Node, FuncDef, NameExpr, MemberExpr, RefExpr, MypyFile, FuncItem, ClassDef, AssignmentStmt,
11-
ImportFrom, TypeInfo, SymbolTable, Var, UNBOUND_IMPORTED, GDEF
11+
ImportFrom, Import, TypeInfo, SymbolTable, Var, UNBOUND_IMPORTED, GDEF
1212
)
1313
from mypy.traverser import TraverserVisitor
1414

@@ -78,6 +78,21 @@ def visit_import_from(self, node: ImportFrom) -> None:
7878
symnode.kind = UNBOUND_IMPORTED
7979
symnode.node = None
8080

81+
def visit_import(self, node: Import) -> None:
82+
if node.assignments:
83+
node.assignments = []
84+
else:
85+
if self.names:
86+
# Reset entries in the symbol table. This is necessary since
87+
# otherwise the semantic analyzer will think that the import
88+
# assigns to an existing name instead of defining a new one.
89+
for name, as_name in node.ids:
90+
imported_name = as_name or name
91+
initial = imported_name.split('.')[0]
92+
symnode = self.names[initial]
93+
symnode.kind = UNBOUND_IMPORTED
94+
symnode.node = None
95+
8196
def visit_name_expr(self, node: NameExpr) -> None:
8297
# Global assignments are processed in semantic analysis pass 1, and we
8398
# only want to strip changes made in passes 2 or later.

mypy/server/target.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
from typing import Iterable, Tuple, List
1+
from typing import Iterable, Tuple, List, Optional
22

33

4-
def module_prefix(modules: Iterable[str], target: str) -> str:
5-
return split_target(modules, target)[0]
4+
def module_prefix(modules: Iterable[str], target: str) -> Optional[str]:
5+
result = split_target(modules, target)
6+
if result is None:
7+
return None
8+
return result[0]
69

710

8-
def split_target(modules: Iterable[str], target: str) -> Tuple[str, str]:
11+
def split_target(modules: Iterable[str], target: str) -> Optional[Tuple[str, str]]:
912
remaining = [] # type: List[str]
1013
while True:
1114
if target in modules:
1215
return target, '.'.join(remaining)
1316
components = target.rsplit('.', 1)
1417
if len(components) == 1:
15-
assert False, 'Cannot find module prefix for {}'.format(target)
18+
return None
1619
target = components[0]
1720
remaining.insert(0, components[1])

0 commit comments

Comments
 (0)