Skip to content

Commit c3fa852

Browse files
Michael0x2agvanrossum
authored andcommitted
Fix imports being unsilenced when checking stale SCCs (#2037)
* Add more logging to find_cache_meta * Fix silenced files being ignored on multiple runs This commit fixes a bug where silenced imports were being unsilenced after incremental mode + silent_imports was run several times after forcing an SCC to be rechecked.
1 parent fc03d43 commit c3fa852

File tree

1 file changed

+46
-5
lines changed

1 file changed

+46
-5
lines changed

mypy/build.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
PYTHON_EXTENSIONS = ['.pyi', '.py']
4949

5050

51+
Graph = Dict[str, 'State']
52+
53+
5154
class BuildResult:
5255
"""The result of a successful build.
5356
@@ -703,12 +706,14 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
703706
id, path, manager.options.cache_dir, manager.options.python_version)
704707
manager.trace('Looking for {} {}'.format(id, data_json))
705708
if not os.path.exists(meta_json):
709+
manager.trace('Could not load cache for {}: could not find {}'.format(id, meta_json))
706710
return None
707711
with open(meta_json, 'r') as f:
708712
meta_str = f.read()
709713
manager.trace('Meta {} {}'.format(id, meta_str.rstrip()))
710714
meta = json.loads(meta_str) # TODO: Errors
711715
if not isinstance(meta, dict):
716+
manager.trace('Could not load cache for {}: meta cache is not a dict'.format(id))
712717
return None
713718
path = os.path.abspath(path)
714719
m = CacheMeta(
@@ -728,32 +733,36 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
728733
if (m.id != id or m.path != path or
729734
m.mtime is None or m.size is None or
730735
m.dependencies is None or m.data_mtime is None):
736+
manager.trace('Metadata abandoned for {}: attributes are missing'.format(id))
731737
return None
732738

733739
# Ignore cache if generated by an older mypy version.
734740
if (m.version_id != manager.version_id
735741
or m.options is None
736742
or len(m.dependencies) != len(m.dep_prios)):
743+
manager.trace('Metadata abandoned for {}: new attributes are missing'.format(id))
737744
return None
738745

739746
# Ignore cache if (relevant) options aren't the same.
740747
cached_options = m.options
741748
current_options = select_options_affecting_cache(manager.options)
742749
if cached_options != current_options:
750+
manager.trace('Metadata abandoned for {}: options differ'.format(id))
743751
return None
744752

745753
# TODO: Share stat() outcome with find_module()
746754
st = os.stat(path) # TODO: Errors
747755
if st.st_mtime != m.mtime or st.st_size != m.size:
748-
manager.log('Metadata abandoned because of modified file {}'.format(path))
756+
manager.log('Metadata abandoned for {}: file {} is modified'.format(id, path))
749757
return None
750758

751759
# It's a match on (id, path, mtime, size).
752760
# Check data_json; assume if its mtime matches it's good.
753761
# TODO: stat() errors
754762
if os.path.getmtime(data_json) != m.data_mtime:
763+
manager.log('Metadata abandoned for {}: data cache is modified'.format(id))
755764
return None
756-
manager.log('Found {} {}'.format(id, meta_json))
765+
manager.log('Found {} {} (metadata is fresh)'.format(id, meta_json))
757766
return m
758767

759768

@@ -1200,6 +1209,40 @@ def fix_cross_refs(self) -> None:
12001209
def calculate_mros(self) -> None:
12011210
fixup_module_pass_two(self.tree, self.manager.modules)
12021211

1212+
def fix_suppressed_dependencies(self, graph: Graph) -> None:
1213+
"""Corrects whether dependencies are considered stale or not when using silent_imports.
1214+
1215+
This method is a hack to correct imports in silent_imports + incremental mode.
1216+
In particular, the problem is that when running mypy with a cold cache, the
1217+
`parse_file(...)` function is called *at the start* of the `load_graph(...)` function.
1218+
Note that load_graph will mark some dependencies as suppressed if they weren't specified
1219+
on the command line in silent_imports mode.
1220+
1221+
However, if the interface for a module is changed, parse_file will be called within
1222+
`process_stale_scc` -- *after* load_graph is finished, wiping out the changes load_graph
1223+
previously made.
1224+
1225+
This method is meant to be run after parse_file finishes in process_stale_scc and will
1226+
recompute what modules should be considered suppressed in silent_import mode.
1227+
"""
1228+
# TODO: See if it's possible to move this check directly into parse_file in some way.
1229+
# TODO: Find a way to write a test case for this fix.
1230+
silent_mode = self.manager.options.silent_imports or self.manager.options.almost_silent
1231+
if not silent_mode:
1232+
return
1233+
1234+
new_suppressed = []
1235+
new_dependencies = []
1236+
entry_points = self.manager.source_set.source_modules
1237+
for dep in self.dependencies + self.suppressed:
1238+
ignored = dep in self.suppressed and dep not in entry_points
1239+
if ignored or dep not in graph:
1240+
new_suppressed.append(dep)
1241+
else:
1242+
new_dependencies.append(dep)
1243+
self.dependencies = new_dependencies
1244+
self.suppressed = new_suppressed
1245+
12031246
# Methods for processing modules from source code.
12041247

12051248
def parse_file(self) -> None:
@@ -1325,9 +1368,6 @@ def write_cache(self) -> None:
13251368
self.manager)
13261369

13271370

1328-
Graph = Dict[str, State]
1329-
1330-
13311371
def dispatch(sources: List[BuildSource], manager: BuildManager) -> None:
13321372
manager.log("Mypy version %s" % __version__)
13331373
graph = load_graph(sources, manager)
@@ -1556,6 +1596,7 @@ def process_stale_scc(graph: Graph, scc: List[str]) -> None:
15561596
# We may already have parsed the module, or not.
15571597
# If the former, parse_file() is a no-op.
15581598
graph[id].parse_file()
1599+
graph[id].fix_suppressed_dependencies(graph)
15591600
for id in scc:
15601601
graph[id].patch_parent()
15611602
for id in scc:

0 commit comments

Comments
 (0)