Skip to content

Commit b67cf51

Browse files
committed
Make metas more compact; fix indirect suppression
1 parent e1643ae commit b67cf51

File tree

2 files changed

+52
-63
lines changed

2 files changed

+52
-63
lines changed

mypy/build.py

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ class CacheMeta(NamedTuple):
363363
# dep_prios and dep_lines are in parallel with dependencies + suppressed
364364
dep_prios: list[int]
365365
dep_lines: list[int]
366-
dep_hashes: dict[str, str]
366+
dep_hashes: list[str]
367367
interface_hash: str # hash representing the public interface
368368
error_lines: list[str]
369369
version_id: str # mypy version for cache invalidation
@@ -403,7 +403,7 @@ def cache_meta_from_dict(meta: dict[str, Any], data_json: str) -> CacheMeta:
403403
meta.get("options"),
404404
meta.get("dep_prios", []),
405405
meta.get("dep_lines", []),
406-
meta.get("dep_hashes", {}),
406+
meta.get("dep_hashes", []),
407407
meta.get("interface_hash", ""),
408408
meta.get("error_lines", []),
409409
meta.get("version_id", sentinel),
@@ -1310,8 +1310,7 @@ def get_cache_names(id: str, path: str, options: Options) -> tuple[str, str, str
13101310
Args:
13111311
id: module ID
13121312
path: module path
1313-
cache_dir: cache directory
1314-
pyversion: Python version (major, minor)
1313+
options: build options
13151314
13161315
Returns:
13171316
A tuple with the file names to be used for the meta JSON, the
@@ -1328,7 +1327,7 @@ def get_cache_names(id: str, path: str, options: Options) -> tuple[str, str, str
13281327
# Solve this by rewriting the paths as relative to the root dir.
13291328
# This only makes sense when using the filesystem backed cache.
13301329
root = _cache_dir_prefix(options)
1331-
return (os.path.relpath(pair[0], root), os.path.relpath(pair[1], root), None)
1330+
return os.path.relpath(pair[0], root), os.path.relpath(pair[1], root), None
13321331
prefix = os.path.join(*id.split("."))
13331332
is_package = os.path.basename(path).startswith("__init__.py")
13341333
if is_package:
@@ -1341,7 +1340,20 @@ def get_cache_names(id: str, path: str, options: Options) -> tuple[str, str, str
13411340
data_suffix = ".data.ff"
13421341
else:
13431342
data_suffix = ".data.json"
1344-
return (prefix + ".meta.json", prefix + data_suffix, deps_json)
1343+
return prefix + ".meta.json", prefix + data_suffix, deps_json
1344+
1345+
1346+
def options_snapshot(id: str, manager: BuildManager) -> dict[str, object]:
1347+
"""Make compact snapshot of options for a module.
1348+
1349+
Separately stor only the options we may compare individually, and take a hash
1350+
of everything else. If --debug-cache is specified, fall back to full snapshot.
1351+
"""
1352+
snapshot = manager.options.clone_for_module(id).select_options_affecting_cache()
1353+
if manager.options.debug_cache:
1354+
return snapshot
1355+
platform_opt = snapshot.pop("platform")
1356+
return {"platform": platform_opt, "other_options": hash_digest(json_dumps(snapshot))}
13451357

13461358

13471359
def find_cache_meta(id: str, path: str, manager: BuildManager) -> CacheMeta | None:
@@ -1403,7 +1415,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> CacheMeta | No
14031415
# Ignore cache if (relevant) options aren't the same.
14041416
# Note that it's fine to mutilate cached_options since it's only used here.
14051417
cached_options = m.options
1406-
current_options = manager.options.clone_for_module(id).select_options_affecting_cache()
1418+
current_options = options_snapshot(id, manager)
14071419
if manager.options.skip_version_check:
14081420
# When we're lax about version we're also lax about platform.
14091421
cached_options["platform"] = current_options["platform"]
@@ -1556,7 +1568,7 @@ def validate_meta(
15561568
"data_mtime": meta.data_mtime,
15571569
"dependencies": meta.dependencies,
15581570
"suppressed": meta.suppressed,
1559-
"options": (manager.options.clone_for_module(id).select_options_affecting_cache()),
1571+
"options": options_snapshot(id, manager),
15601572
"dep_prios": meta.dep_prios,
15611573
"dep_lines": meta.dep_lines,
15621574
"dep_hashes": meta.dep_hashes,
@@ -1701,7 +1713,6 @@ def write_cache(
17011713
# updates made by inline config directives in the file. This is
17021714
# important, or otherwise the options would never match when
17031715
# verifying the cache.
1704-
options = manager.options.clone_for_module(id)
17051716
assert source_hash is not None
17061717
meta = {
17071718
"id": id,
@@ -1712,7 +1723,7 @@ def write_cache(
17121723
"data_mtime": data_mtime,
17131724
"dependencies": dependencies,
17141725
"suppressed": suppressed,
1715-
"options": options.select_options_affecting_cache(),
1726+
"options": options_snapshot(id, manager),
17161727
"dep_prios": dep_prios,
17171728
"dep_lines": dep_lines,
17181729
"interface_hash": interface_hash,
@@ -2029,7 +2040,10 @@ def __init__(
20292040
self.priorities = {id: pri for id, pri in zip(all_deps, self.meta.dep_prios)}
20302041
assert len(all_deps) == len(self.meta.dep_lines)
20312042
self.dep_line_map = {id: line for id, line in zip(all_deps, self.meta.dep_lines)}
2032-
self.dep_hashes = self.meta.dep_hashes
2043+
assert len(self.meta.dep_hashes) == len(self.meta.dependencies)
2044+
self.dep_hashes = {
2045+
k: v for (k, v) in zip(self.meta.dependencies, self.meta.dep_hashes)
2046+
}
20332047
self.error_lines = self.meta.error_lines
20342048
if temporary:
20352049
self.load_tree(temporary=True)
@@ -2346,6 +2360,7 @@ def compute_dependencies(self) -> None:
23462360
self.suppressed_set = set()
23472361
self.priorities = {} # id -> priority
23482362
self.dep_line_map = {} # id -> line
2363+
self.dep_hashes = {}
23492364
dep_entries = manager.all_imported_modules_in_file(
23502365
self.tree
23512366
) + self.manager.plugin.get_additional_deps(self.tree)
@@ -2433,7 +2448,7 @@ def finish_passes(self) -> None:
24332448

24342449
# We should always patch indirect dependencies, even in full (non-incremental) builds,
24352450
# because the cache still may be written, and it must be correct.
2436-
self._patch_indirect_dependencies(
2451+
self.patch_indirect_dependencies(
24372452
# Two possible sources of indirect dependencies:
24382453
# * Symbols not directly imported in this module but accessed via an attribute
24392454
# or via a re-export (vast majority of these recorded in semantic analysis).
@@ -2470,21 +2485,17 @@ def free_state(self) -> None:
24702485
self._type_checker.reset()
24712486
self._type_checker = None
24722487

2473-
def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:
2474-
assert None not in types
2475-
valid = self.valid_references()
2488+
def patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:
2489+
assert self.ancestors is not None
2490+
existing_deps = set(self.dependencies + self.suppressed + self.ancestors)
2491+
existing_deps.add(self.id)
24762492

24772493
encountered = self.manager.indirection_detector.find_modules(types) | module_refs
2478-
extra = encountered - valid
2479-
2480-
for dep in sorted(extra):
2494+
for dep in sorted(encountered - existing_deps):
24812495
if dep not in self.manager.modules:
24822496
continue
2483-
if dep not in self.suppressed_set and dep not in self.manager.missing_modules:
2484-
self.add_dependency(dep)
2485-
self.priorities[dep] = PRI_INDIRECT
2486-
elif dep not in self.suppressed_set and dep in self.manager.missing_modules:
2487-
self.suppress_dependency(dep)
2497+
self.add_dependency(dep)
2498+
self.priorities[dep] = PRI_INDIRECT
24882499

24892500
def compute_fine_grained_deps(self) -> dict[str, set[str]]:
24902501
assert self.tree is not None
@@ -2514,16 +2525,6 @@ def update_fine_grained_deps(self, deps: dict[str, set[str]]) -> None:
25142525
merge_dependencies(self.compute_fine_grained_deps(), deps)
25152526
type_state.update_protocol_deps(deps)
25162527

2517-
def valid_references(self) -> set[str]:
2518-
assert self.ancestors is not None
2519-
valid_refs = set(self.dependencies + self.suppressed + self.ancestors)
2520-
valid_refs.add(self.id)
2521-
2522-
if "os" in valid_refs:
2523-
valid_refs.add("os.path")
2524-
2525-
return valid_refs
2526-
25272528
def write_cache(self) -> tuple[dict[str, Any], str] | None:
25282529
assert self.tree is not None, "Internal error: method must be called on parsed file only"
25292530
# We don't support writing cache files in fine-grained incremental mode.
@@ -2577,14 +2578,16 @@ def verify_dependencies(self, suppressed_only: bool = False) -> None:
25772578
"""
25782579
manager = self.manager
25792580
assert self.ancestors is not None
2581+
# Strip out indirect dependencies. See comment in build.load_graph().
25802582
if suppressed_only:
2581-
all_deps = self.suppressed
2583+
all_deps = [dep for dep in self.suppressed if self.priorities.get(dep) != PRI_INDIRECT]
25822584
else:
2583-
# Strip out indirect dependencies. See comment in build.load_graph().
25842585
dependencies = [
2585-
dep for dep in self.dependencies if self.priorities.get(dep) != PRI_INDIRECT
2586+
dep
2587+
for dep in self.dependencies + self.suppressed
2588+
if self.priorities.get(dep) != PRI_INDIRECT
25862589
]
2587-
all_deps = dependencies + self.suppressed + self.ancestors
2590+
all_deps = dependencies + self.ancestors
25882591
for dep in all_deps:
25892592
if dep in manager.modules:
25902593
continue
@@ -3250,6 +3253,13 @@ def load_graph(
32503253
if dep in graph and dep in st.suppressed_set:
32513254
# Previously suppressed file is now visible
32523255
st.add_dependency(dep)
3256+
# In the loop above we skip indirect dependencies, so to make indirect dependencies behave
3257+
# more consistently with regular ones, we suppress them manually here (when needed).
3258+
for st in graph.values():
3259+
indirect = [dep for dep in st.dependencies if st.priorities.get(dep) == PRI_INDIRECT]
3260+
for dep in indirect:
3261+
if dep not in graph:
3262+
st.suppress_dependency(dep)
32533263
manager.plugin.set_modules(manager.modules)
32543264
return graph
32553265

@@ -3284,8 +3294,9 @@ def find_stale_sccs(
32843294
32853295
Fresh SCCs are those where:
32863296
* We have valid cache files for all modules in the SCC.
3297+
* There are no changes in dependencies (files removed from/added to the build).
32873298
* The interface hashes of direct dependents matches those recorded in the cache.
3288-
* There are no new (un)suppressed dependencies (files removed/added to the build).
3299+
The first and second conditions are verified by is_fresh().
32893300
"""
32903301
stale_sccs = []
32913302
fresh_sccs = []
@@ -3294,34 +3305,15 @@ def find_stale_sccs(
32943305
fresh = not stale_scc
32953306

32963307
# Verify that interfaces of dependencies still present in graph are up-to-date (fresh).
3297-
# Note: if a dependency is not in graph anymore, it should be considered interface-stale.
3298-
# This is important to trigger any relevant updates from indirect dependencies that were
3299-
# removed in load_graph().
33003308
stale_deps = set()
33013309
for id in ascc.mod_ids:
33023310
for dep in graph[id].dep_hashes:
3303-
if dep not in graph:
3304-
stale_deps.add(dep)
3305-
continue
3306-
if graph[dep].interface_hash != graph[id].dep_hashes[dep]:
3311+
if dep in graph and graph[dep].interface_hash != graph[id].dep_hashes[dep]:
33073312
stale_deps.add(dep)
33083313
fresh = fresh and not stale_deps
33093314

3310-
undeps = set()
3311-
if fresh:
3312-
# Check if any dependencies that were suppressed according
3313-
# to the cache have been added back in this run.
3314-
# NOTE: Newly suppressed dependencies are handled by is_fresh().
3315-
for id in ascc.mod_ids:
3316-
undeps.update(graph[id].suppressed)
3317-
undeps &= graph.keys()
3318-
if undeps:
3319-
fresh = False
3320-
33213315
if fresh:
33223316
fresh_msg = "fresh"
3323-
elif undeps:
3324-
fresh_msg = f"stale due to changed suppression ({' '.join(sorted(undeps))})"
33253317
elif stale_scc:
33263318
fresh_msg = "inherently stale"
33273319
if stale_scc != ascc.mod_ids:
@@ -3563,9 +3555,7 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None:
35633555
if meta_tuple is None:
35643556
continue
35653557
meta, meta_json = meta_tuple
3566-
meta["dep_hashes"] = {
3567-
dep: graph[dep].interface_hash for dep in graph[id].dependencies if dep in graph
3568-
}
3558+
meta["dep_hashes"] = [graph[dep].interface_hash for dep in graph[id].dependencies]
35693559
meta["error_lines"] = errors_by_id.get(id, [])
35703560
write_cache_meta(meta, manager, meta_json)
35713561
manager.done_sccs.add(ascc.id)

mypy/options.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66
import sysconfig
77
import warnings
8-
from collections.abc import Mapping
98
from re import Pattern
109
from typing import Any, Callable, Final
1110

@@ -621,7 +620,7 @@ def compile_glob(self, s: str) -> Pattern[str]:
621620
expr += re.escape("." + part) if part != "*" else r"(\..*)?"
622621
return re.compile(expr + "\\Z")
623622

624-
def select_options_affecting_cache(self) -> Mapping[str, object]:
623+
def select_options_affecting_cache(self) -> dict[str, object]:
625624
result: dict[str, object] = {}
626625
for opt in OPTIONS_AFFECTING_CACHE:
627626
val = getattr(self, opt)

0 commit comments

Comments
 (0)