@@ -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
13471359def 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 )
0 commit comments