@@ -2055,14 +2055,26 @@ def find_cache_meta(
20552055
20562056 # Ignore cache if (relevant) options aren't the same.
20572057 # Note that it's fine to mutilate cached_options since it's only used here.
2058- cached_options = m .options
2058+ cached_options = m .options . copy ()
20592059 current_options = options_snapshot (id , manager )
20602060 if manager .options .skip_version_check :
20612061 # When we're lax about version we're also lax about platform.
20622062 cached_options ["platform" ] = current_options ["platform" ]
20632063 if "debug_cache" in cached_options :
20642064 # Older versions included debug_cache, but it's silly to compare it.
20652065 del cached_options ["debug_cache" ]
2066+ if "local_partial_types" in cached_options :
2067+ local_partial_types = cached_options ["local_partial_types" ]
2068+ del cached_options ["local_partial_types" ]
2069+ is_parallel = manager .options .num_workers > 0
2070+ if not local_partial_types and cached_options ["is_parallel" ] != is_parallel :
2071+ # If local partial types are disabled, behavior is too different for
2072+ # parallel and sequential runs, see write_cache() for details.
2073+ return None
2074+ del cached_options ["is_parallel" ]
2075+ else :
2076+ # Cache from an old mypy version.
2077+ return None
20662078 if cached_options != current_options :
20672079 manager .log (f"Metadata abandoned for { id } : options differ" )
20682080 if manager .options .verbosity >= 2 :
@@ -2226,7 +2238,6 @@ def validate_meta(
22262238 meta .mtime = mtime
22272239 meta .path = path
22282240 meta .size = size
2229- meta .options = options_snapshot (id , manager )
22302241 meta_file , _ , _ = get_cache_names (id , path , manager .options )
22312242 if manager .logging_enabled :
22322243 manager .log (
@@ -2271,6 +2282,8 @@ def write_cache(
22712282 trans_dep_hash : bytes ,
22722283 source_hash : str ,
22732284 ignore_all : bool ,
2285+ local_partial_types : bool ,
2286+ is_parallel : bool ,
22742287 manager : BuildManager ,
22752288) -> tuple [bytes , tuple [CacheMeta , str ] | None ]:
22762289 """Write cache files for a module.
@@ -2370,6 +2383,13 @@ def write_cache(
23702383 # important, or otherwise the options would never match when
23712384 # verifying the cache.
23722385 assert source_hash is not None
2386+ # Local partial types need special handling as they behave differently
2387+ # in sequential and parallel runs:
2388+ # * In sequential run we respected the option, but use different SCC
2389+ # processing logic depending on whether they are enabled or disabled.
2390+ # * In parallel run they are always on, and we give an error if a user
2391+ # tries to disable them.
2392+ extra_options = {"local_partial_types" : local_partial_types , "is_parallel" : is_parallel }
23732393 meta = CacheMeta (
23742394 id = id ,
23752395 path = path ,
@@ -2381,7 +2401,7 @@ def write_cache(
23812401 data_file = data_file ,
23822402 suppressed = suppressed ,
23832403 imports_ignored = imports_ignored ,
2384- options = options_snapshot (id , manager ),
2404+ options = options_snapshot (id , manager ) | extra_options ,
23852405 suppressed_deps_opts = suppressed_deps_opts ,
23862406 dep_prios = dep_prios ,
23872407 dep_lines = dep_lines ,
@@ -2726,6 +2746,11 @@ def new_state(
27262746 meta , meta_ex = meta_pair
27272747 interface_hash = meta .interface_hash
27282748 meta_source_hash = meta .hash
2749+ # Update the local partial types in case they were set by an inline config.
2750+ # So we can select the correct SCC processing logic without reading the file.
2751+ local_partial_types = meta .options ["local_partial_types" ]
2752+ if options .local_partial_types != local_partial_types :
2753+ options = options .apply_changes ({"local_partial_types" : local_partial_types })
27292754 if path and source is None and manager .fscache .isdir (path ):
27302755 source = ""
27312756
@@ -3636,6 +3661,8 @@ def write_cache(self) -> tuple[CacheMeta, str] | None:
36363661 self .trans_dep_hash ,
36373662 self .source_hash ,
36383663 self .ignore_all ,
3664+ self .options .local_partial_types ,
3665+ self .options .num_workers > 0 ,
36393666 self .manager ,
36403667 )
36413668 if new_interface_hash == self .interface_hash :
@@ -4636,18 +4663,7 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
46364663 # type-checking this is already done and results should be empty here.
46374664 if not manager .workers :
46384665 assert not results
4639- for id , result in results .items ():
4640- # Interface and implementation results may be mixed in the same batch
4641- # from different workers, process each one accordingly.
4642- if result .interface_hash is not None :
4643- new_hash = bytes .fromhex (result .interface_hash )
4644- if new_hash != graph [id ].interface_hash :
4645- graph [id ].mark_interface_stale ()
4646- graph [id ].interface_hash = new_hash
4647- else :
4648- manager .flush_errors (
4649- manager .errors .simplify_path (graph [id ].xpath ), result .error_lines , False
4650- )
4666+ process_results (results , graph , manager )
46514667 ready = []
46524668 for done_scc in done :
46534669 for dependent in done_scc .direct_dependents :
@@ -4659,6 +4675,26 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
46594675 manager .trace (f"Transitive deps cache size: { sys .getsizeof (manager .transitive_deps_cache )} " )
46604676
46614677
4678+ def process_results (results : dict [str , ModuleResult ], graph : Graph , manager : BuildManager ) -> None :
4679+ """Process results of type-checking given modules.
4680+
4681+ This will update interface hashes and flush type-checking errors (if any).
4682+ Blockers should have been already handled by the caller.
4683+ """
4684+ for id , result in results .items ():
4685+ # Interface and implementation results may be mixed in the same batch
4686+ # from different workers, process each one accordingly.
4687+ if result .interface_hash is not None :
4688+ new_hash = bytes .fromhex (result .interface_hash )
4689+ if new_hash != graph [id ].interface_hash :
4690+ graph [id ].mark_interface_stale ()
4691+ graph [id ].interface_hash = new_hash
4692+ else :
4693+ manager .flush_errors (
4694+ manager .errors .simplify_path (graph [id ].xpath ), result .error_lines , False
4695+ )
4696+
4697+
46624698def order_ascc (graph : Graph , ascc : AbstractSet [str ], pri_max : int = PRI_INDIRECT ) -> list [str ]:
46634699 """Come up with the ideal processing order within an SCC.
46644700
@@ -4776,7 +4812,45 @@ def maybe_load_deps(graph: Graph, ascc: SCC, manager: BuildManager) -> None:
47764812
47774813
47784814def process_stale_scc (graph : Graph , ascc : SCC , manager : BuildManager ) -> None :
4779- """Process the modules in one SCC from source code."""
4815+ """Process the modules in one SCC from source code.
4816+
4817+ This will process module interfaces first (when possible). This mirrors
4818+ how things are done in parallel type checking.
4819+ """
4820+ if not all (graph [id ].options .local_partial_types for id in ascc .mod_ids ):
4821+ # If local partial types are disabled we must process each file sequentially.
4822+ process_stale_scc_full (graph , ascc , manager )
4823+ return
4824+ manager .parse_all ([graph [id ] for id in ascc .mod_ids ], post_parse = False )
4825+ scc_result = process_stale_scc_interface (
4826+ graph , ascc , manager , from_cache = {id for id in ascc .mod_ids if graph [id ].meta }
4827+ )
4828+ manager .commit ()
4829+
4830+ # Process interface results before starting implementations
4831+ # (to mimic parallel checking 1:1).
4832+ mod_results = {}
4833+ stale = []
4834+ meta_files = []
4835+ for id , mod_result , meta_file in scc_result :
4836+ stale .append (id )
4837+ mod_results [id ] = mod_result
4838+ meta_files .append (meta_file )
4839+ process_results (mod_results , graph , manager )
4840+
4841+ mod_results = {}
4842+ for id , meta_file in zip (stale , meta_files ):
4843+ mod_results |= process_stale_scc_implementation (graph , [id ], manager , [meta_file ])
4844+ manager .commit ()
4845+ process_results (mod_results , graph , manager )
4846+
4847+
4848+ def process_stale_scc_full (graph : Graph , ascc : SCC , manager : BuildManager ) -> None :
4849+ """Process the modules in one SCC from source code.
4850+
4851+ This is the legacy function that processes each file sequentially (line-by-line),
4852+ thus it may interleave processing interface and implementation parts.
4853+ """
47804854 # First verify if all transitive dependencies are loaded in the current process.
47814855 t0 = time .time ()
47824856 maybe_load_deps (graph , ascc , manager )
@@ -4879,7 +4953,7 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None:
48794953
48804954def process_stale_scc_interface (
48814955 graph : Graph , ascc : SCC , manager : BuildManager , from_cache : set [str ]
4882- ) -> list [tuple [str , ModuleResult , str ]]:
4956+ ) -> list [tuple [str , ModuleResult , str | None ]]:
48834957 """Process the modules' interfaces in one SCC from source code."""
48844958 # First verify if all transitive dependencies are loaded in the current process.
48854959 t0 = time .time ()
@@ -4925,16 +4999,19 @@ def process_stale_scc_interface(
49254999 for id in stale :
49265000 meta_tuple = meta_tuples [id ]
49275001 if meta_tuple is None :
4928- continue
4929- meta , meta_file = meta_tuple
5002+ meta = meta_file = None
5003+ else :
5004+ meta , meta_file = meta_tuple
49305005 state = graph [id ]
4931- meta .dep_hashes = [
4932- graph [dep ].interface_hash
4933- for dep in state .dependencies
4934- if state .priorities .get (dep ) != PRI_INDIRECT
4935- ]
4936- write_cache_meta (meta , manager , meta_file )
4937- manager .commit_module (meta_file )
5006+ if meta is not None :
5007+ assert meta_file is not None
5008+ meta .dep_hashes = [
5009+ graph [dep ].interface_hash
5010+ for dep in state .dependencies
5011+ if state .priorities .get (dep ) != PRI_INDIRECT
5012+ ]
5013+ write_cache_meta (meta , manager , meta_file )
5014+ manager .commit_module (meta_file )
49385015 scc_result .append ((id , ModuleResult (graph [id ].interface_hash .hex (), []), meta_file ))
49395016 manager .done_sccs .add (ascc .id )
49405017 manager .add_stats (
@@ -4948,7 +5025,7 @@ def process_stale_scc_interface(
49485025
49495026
49505027def process_stale_scc_implementation (
4951- graph : Graph , stale : list [str ], manager : BuildManager , meta_files : list [str ]
5028+ graph : Graph , stale : list [str ], manager : BuildManager , meta_files : list [str | None ]
49525029) -> dict [str , ModuleResult ]:
49535030 """Process implementations (top-level function/method bodies) in an SCC."""
49545031 t0 = time .time ()
@@ -4963,7 +5040,10 @@ def process_stale_scc_implementation(
49635040 continue
49645041 # We need to reset deferral count after possibly deferring any methods that
49655042 # are considered part of the top-level (because they define/infer variables).
4966- checker .pass_num = 0
5043+ # Note we need to add one pass to compensate for function bodies not visited in
5044+ # type_check_first_pass(). So with current DEFAULT_LAST_PASS = 2 each function
5045+ # will be visited at most three times, for both single-phase and two-phase logic.
5046+ checker .pass_num = - 1
49675047 checker .deferred_nodes .clear ()
49685048 tree = graph [id ].tree
49695049 assert tree is not None
@@ -4993,27 +5073,28 @@ def process_stale_scc_implementation(
49935073 scc_result = {}
49945074 for id , meta_file in zip (stale , meta_files ):
49955075 state = graph [id ]
5076+ # If there are no errors, only write the cache, don't send anything back
5077+ # to the caller (as a micro-optimization).
5078+ if graph [id ].xpath not in manager .errors .ignored_files :
5079+ errors = manager .errors .file_messages (graph [id ].xpath )
5080+ formatted = manager .errors .format_messages (
5081+ graph [id ].xpath , errors , formatter = manager .error_formatter
5082+ )
5083+ scc_result [id ] = ModuleResult (None , formatted )
5084+ else :
5085+ errors = []
5086+ if meta_file is None :
5087+ continue
49965088 indirect = [dep for dep in state .dependencies if state .priorities .get (dep ) == PRI_INDIRECT ]
49975089 meta_ex = CacheMetaEx (
49985090 dependencies = indirect ,
49995091 suppressed = [
50005092 dep for dep in state .suppressed if state .priorities .get (dep ) == PRI_INDIRECT
50015093 ],
50025094 dep_hashes = [graph [dep ].interface_hash for dep in indirect ],
5003- error_lines = [] ,
5095+ error_lines = errors ,
50045096 )
5005- if graph [id ].xpath not in manager .errors .ignored_files :
5006- errors = manager .errors .file_messages (graph [id ].xpath )
5007- formatted = manager .errors .format_messages (
5008- graph [id ].xpath , errors , formatter = manager .error_formatter
5009- )
5010- meta_ex .error_lines = errors
5011- write_cache_meta_ex (meta_file , meta_ex , manager )
5012- scc_result [id ] = ModuleResult (None , formatted )
5013- else :
5014- # If there are no errors, only write the cache, don't send anything back
5015- # to the caller (as a micro-optimization).
5016- write_cache_meta_ex (meta_file , meta_ex , manager )
5097+ write_cache_meta_ex (meta_file , meta_ex , manager )
50175098 manager .commit_module (meta_file )
50185099
50195100 manager .add_stats (type_check_time_implementation = time .time () - t0 )
0 commit comments