Skip to content

Commit d84bdea

Browse files
committed
Fix interaction between --incremental and --silent-imports. (#1383)
(Still needs tests; see #1349.)
1 parent abf8259 commit d84bdea

File tree

2 files changed

+50
-9
lines changed

2 files changed

+50
-9
lines changed

mypy/build.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,14 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int],
315315
('path', str),
316316
('mtime', float),
317317
('size', int),
318-
('dependencies', List[str]),
318+
('dependencies', List[str]), # names of imported modules
319319
('data_mtime', float), # mtime of data_json
320320
('data_json', str), # path of <id>.data.json
321+
('suppressed', List[str]), # dependencies that weren't imported
321322
])
323+
# NOTE: dependencies + suppressed == all unreachable imports;
324+
# suppressed contains those reachable imports that were prevented by
325+
# --silent-imports or simply not found.
322326

323327

324328
class BuildManager:
@@ -684,9 +688,10 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
684688
meta.get('path'),
685689
meta.get('mtime'),
686690
meta.get('size'),
687-
meta.get('dependencies'),
691+
meta.get('dependencies', []),
688692
meta.get('data_mtime'),
689693
data_json,
694+
meta.get('suppressed', []),
690695
)
691696
if (m.id != id or m.path != path or
692697
m.mtime is None or m.size is None or
@@ -710,7 +715,8 @@ def random_string():
710715
return binascii.hexlify(os.urandom(8)).decode('ascii')
711716

712717

713-
def write_cache(id: str, path: str, tree: MypyFile, dependencies: List[str],
718+
def write_cache(id: str, path: str, tree: MypyFile,
719+
dependencies: List[str], suppressed: List[str],
714720
manager: BuildManager) -> None:
715721
"""Write cache files for a module.
716722
@@ -719,6 +725,7 @@ def write_cache(id: str, path: str, tree: MypyFile, dependencies: List[str],
719725
path: module path
720726
tree: the fully checked module data
721727
dependencies: module IDs on which this module depends
728+
suppressed: module IDs which were suppressed as dependencies
722729
manager: the build manager (for pyversion, log/trace)
723730
"""
724731
path = os.path.abspath(path)
@@ -746,6 +753,7 @@ def write_cache(id: str, path: str, tree: MypyFile, dependencies: List[str],
746753
'size': size,
747754
'data_mtime': data_mtime,
748755
'dependencies': dependencies,
756+
'suppressed': suppressed,
749757
}
750758
with open(meta_json_tmp, 'w') as f:
751759
json.dump(meta, f, sort_keys=True)
@@ -919,6 +927,7 @@ class State:
919927
data = None # type: Optional[str]
920928
tree = None # type: Optional[MypyFile]
921929
dependencies = None # type: List[str]
930+
suppressed = None # type: List[str] # Suppressed/missing dependencies
922931

923932
# Map each dependency to the line number where it is first imported
924933
dep_line_map = None # type: Dict[str, int]
@@ -1017,11 +1026,15 @@ def __init__(self,
10171026
# TODO: Get mtime if not cached.
10181027
self.add_ancestors()
10191028
if self.meta:
1020-
self.dependencies = self.meta.dependencies
1029+
# Make copies, since we may modify these and want to
1030+
# compare them to the originals later.
1031+
self.dependencies = list(self.meta.dependencies)
1032+
self.suppressed = list(self.meta.suppressed)
10211033
self.dep_line_map = {}
10221034
else:
10231035
# Parse the file (and then some) to get the dependencies.
10241036
self.parse_file()
1037+
self.suppressed = []
10251038

10261039
def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None:
10271040
# TODO: Read the path (the __init__.py file) and return
@@ -1064,9 +1077,13 @@ def add_ancestors(self) -> None:
10641077

10651078
def is_fresh(self) -> bool:
10661079
"""Return whether the cache data for this file is fresh."""
1067-
return self.meta is not None
1080+
# NOTE: self.dependencies may differ from
1081+
# self.meta.dependencies when a dependency is dropped due to
1082+
# suppression by --silent-imports. However when a suppressed
1083+
# dependency is added back we find out later in the process.
1084+
return self.meta is not None and self.dependencies == self.meta.dependencies
10681085

1069-
def clear_fresh(self) -> None:
1086+
def mark_stale(self) -> None:
10701087
"""Throw away the cache data for this file, marking it as stale."""
10711088
self.meta = None
10721089

@@ -1147,17 +1164,24 @@ def parse_file(self) -> None:
11471164
# Add all direct imports (this is why we needed the first pass).
11481165
# Also keep track of each dependency's source line.
11491166
dependencies = []
1167+
suppressed = []
11501168
dep_line_map = {} # type: Dict[str, int] # id -> line
11511169
for id, line in manager.all_imported_modules_in_file(self.tree):
1170+
if id == self.id:
1171+
continue
11521172
# Omit missing modules, as otherwise we could not type-check
11531173
# programs with missing modules.
1154-
if id == self.id or id in manager.missing_modules:
1174+
if id in manager.missing_modules:
1175+
if id not in dep_line_map:
1176+
suppressed.append(id)
1177+
dep_line_map[id] = line
11551178
continue
11561179
if id == '':
11571180
# Must be from a relative import.
11581181
manager.errors.set_file(self.xpath)
11591182
manager.errors.report(line, "No parent module -- cannot perform relative import",
11601183
blocker=True)
1184+
continue
11611185
if id not in dep_line_map:
11621186
dependencies.append(id)
11631187
dep_line_map[id] = line
@@ -1172,6 +1196,7 @@ def parse_file(self) -> None:
11721196
# explicitly checked for this, but ran afoul of other reasons
11731197
# for differences (e.g. --silent-imports).
11741198
self.dependencies = dependencies
1199+
self.suppressed = suppressed
11751200
self.dep_line_map = dep_line_map
11761201
self.check_blockers()
11771202

@@ -1211,7 +1236,9 @@ def type_check(self) -> None:
12111236

12121237
def write_cache(self) -> None:
12131238
if self.path and INCREMENTAL in self.manager.flags and not self.manager.errors.is_errors():
1214-
write_cache(self.id, self.path, self.tree, list(self.dependencies), self.manager)
1239+
write_cache(self.id, self.path, self.tree,
1240+
list(self.dependencies), list(self.suppressed),
1241+
self.manager)
12151242

12161243

12171244
Graph = Dict[str, State]
@@ -1260,6 +1287,7 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:
12601287
except ModuleNotFound:
12611288
if dep in st.dependencies:
12621289
st.dependencies.remove(dep)
1290+
st.suppressed.append(dep)
12631291
else:
12641292
assert newst.id not in graph, newst.id
12651293
graph[newst.id] = newst
@@ -1299,6 +1327,16 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
12991327
deps -= ascc
13001328
stale_deps = {id for id in deps if not graph[id].is_fresh()}
13011329
fresh = fresh and not stale_deps
1330+
undeps = set()
1331+
if fresh:
1332+
# Check if any dependencies that were suppressed according
1333+
# to the cache have heen added back in this run.
1334+
# NOTE: Newly suppressed dependencies are handled by is_fresh().
1335+
for id in scc:
1336+
undeps.update(graph[id].suppressed)
1337+
undeps &= graph.keys()
1338+
if undeps:
1339+
fresh = False
13021340
if fresh:
13031341
# All cache files are fresh. Check that no dependency's
13041342
# cache file is newer than any scc node's cache file.
@@ -1325,6 +1363,8 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
13251363
fresh_msg = "out of date by %.0f seconds" % (newest_in_deps - oldest_in_scc)
13261364
else:
13271365
fresh_msg = "fresh"
1366+
elif undeps:
1367+
fresh_msg = "stale due to changed suppression (%s)" % " ".join(sorted(undeps))
13281368
elif stale_scc:
13291369
fresh_msg = "inherently stale (%s)" % " ".join(sorted(stale_scc))
13301370
if stale_deps:
@@ -1357,7 +1397,7 @@ def process_fresh_scc(graph: Graph, scc: List[str]) -> None:
13571397
def process_stale_scc(graph: Graph, scc: List[str]) -> None:
13581398
"""Process the modules in one SCC from source code."""
13591399
for id in scc:
1360-
graph[id].clear_fresh()
1400+
graph[id].mark_stale()
13611401
for id in scc:
13621402
# We may already have parsed the module, or not.
13631403
# If the former, parse_file() is a no-op.

mypy/fixup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def lookup_qualified_stnode(modules: Dict[str, MypyFile], name: str) -> SymbolTa
225225
head = name
226226
rest = []
227227
while True:
228+
assert '.' in head, "Cannot find %s" % (name,)
228229
head, tail = head.rsplit('.', 1)
229230
mod = modules.get(head)
230231
if mod is not None:

0 commit comments

Comments
 (0)