Skip to content

Commit 8ebcdf2

Browse files
committed
Add implicit module reference data to dependencies
This commit modifies the build process to record all implicit module references a given file makes when writing cache data. Previously, the cache data would only save all explicit dependencies (i.e. modules used within a direct import). This posed a problem because it turned out it's trivially easy to construct a series of interlinked files wherein changing the interface of one module produces or fixes an error in a seemingly unrelated file (which did NOT import anything from the first module). This made optimizing incremental mode harder since an interface change could have unknown effects on the codebase as a whole. This change modifies how dependencies are recorded so BOTH explicit (directly imported) and implicit (indirectly reliant) dependences are recorded for each file. This means that checking to see if any of our dependents' interfaces have changed requires simply checking every module in the list of dependencies (moving one "step") rather then having to potentially crawl the entire graph. In order to avoid accidentally perturbing the import graph in unpredictable ways, all implicit dependences will be given a lower priority then regular imports (lower then PRI_LOW).
1 parent e0b6a1b commit 8ebcdf2

File tree

1 file changed

+27
-19
lines changed

1 file changed

+27
-19
lines changed

mypy/build.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
SymbolTableNode, MODULE_REF)
3030
from mypy.semanal import FirstPass, SemanticAnalyzer, ThirdPass
3131
from mypy.checker import TypeChecker
32+
from mypy.indirection import TypeIndirectionVisitor
3233
from mypy.errors import Errors, CompileError, DecodeError, report_internal_error
3334
from mypy import fixup
3435
from mypy.report import Reports
@@ -305,6 +306,7 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int]) -> List[str]:
305306
PRI_HIGH = 5 # top-level "from X import blah"
306307
PRI_MED = 10 # top-level "import X"
307308
PRI_LOW = 20 # either form inside a function
309+
PRI_INDIRECT = 30 # an indirect dependency
308310
PRI_ALL = 99 # include all priorities
309311

310312

@@ -351,6 +353,7 @@ def __init__(self, data_dir: str,
351353
self.modules = self.semantic_analyzer.modules
352354
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
353355
self.type_checker = TypeChecker(self.errors, self.modules, options=options)
356+
self.indirection_detector = TypeIndirectionVisitor()
354357
self.missing_modules = set() # type: Set[str]
355358
self.stale_modules = set() # type: Set[str]
356359
self.rechecked_modules = set() # type: Set[str]
@@ -1415,11 +1418,35 @@ def type_check(self) -> None:
14151418
return
14161419
with self.wrap_context():
14171420
manager.type_checker.visit_file(self.tree, self.xpath)
1421+
1422+
if manager.options.incremental:
1423+
self._patch_indirect_dependencies(manager.type_checker.module_refs)
1424+
14181425
if manager.options.dump_inference_stats:
14191426
dump_type_stats(self.tree, self.xpath, inferred=True,
14201427
typemap=manager.type_checker.type_map)
14211428
manager.report_file(self.tree)
14221429

1430+
def _patch_indirect_dependencies(self, module_refs: Set[str]) -> None:
1431+
types = self.manager.type_checker.module_type_map.values()
1432+
valid = self.valid_references()
1433+
1434+
encountered = self.manager.indirection_detector.find_modules(types) | module_refs
1435+
extra = encountered - valid
1436+
1437+
for dep in sorted(extra):
1438+
self.dependencies.append(dep)
1439+
self.priorities[dep] = PRI_INDIRECT
1440+
1441+
def valid_references(self) -> Set[str]:
1442+
valid_refs = set(self.dependencies + self.suppressed + self.ancestors)
1443+
valid_refs .add(self.id)
1444+
1445+
if "os" in valid_refs:
1446+
valid_refs.add("os.path")
1447+
1448+
return valid_refs
1449+
14231450
def write_cache(self) -> None:
14241451
if self.path and self.manager.options.incremental and not self.manager.errors.is_errors():
14251452
dep_prios = [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
@@ -1598,25 +1625,6 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
15981625
else:
15991626
process_stale_scc(graph, scc)
16001627

1601-
# TODO: This is a workaround to get around the "chaining imports" problem
1602-
# with the interface checks.
1603-
#
1604-
# That is, if we have a file named `module_a.py` which does:
1605-
#
1606-
# import module_b
1607-
# module_b.module_c.foo(3)
1608-
#
1609-
# ...and if the type signature of `module_c.foo(...)` were to change,
1610-
# module_a_ would not be rechecked since the interface of `module_b`
1611-
# would not be considered changed.
1612-
#
1613-
# As a workaround, this check will force a module's interface to be
1614-
# considered stale if anything it imports has a stale interface,
1615-
# which ensures these changes are caught and propagated.
1616-
if len(stale_deps) > 0:
1617-
for id in scc:
1618-
graph[id].mark_interface_stale()
1619-
16201628

16211629
def order_ascc(graph: Graph, ascc: AbstractSet[str], pri_max: int = PRI_ALL) -> List[str]:
16221630
"""Come up with the ideal processing order within an SCC.

0 commit comments

Comments
 (0)