170170from mypy .plugins .default import DefaultPlugin
171171from mypy .renaming import LimitedVariableRenameVisitor , VariableRenameVisitor
172172from mypy .stats import dump_type_stats
173- from mypy .stubinfo import is_module_from_legacy_bundled_package , stub_distribution_name
173+ from mypy .stubinfo import stub_distribution_name
174174from mypy .types import Type , instance_cache
175175from mypy .typestate import reset_global_state , type_state
176176from mypy .util import json_dumps , json_loads
@@ -955,7 +955,7 @@ def __init__(
955955 # until all the files have been added. This means that a
956956 # new file can be processed O(n**2) times. This cache
957957 # avoids most of this redundant work.
958- self .ast_cache : dict [str , tuple [MypyFile , list [ErrorInfo ]]] = {}
958+ self .ast_cache : dict [str , tuple [MypyFile , list [ErrorInfo ], str | None ]] = {}
959959 # Number of times we used GC optimization hack for fresh SCCs.
960960 self .gc_freeze_cycles = 0
961961 # Mapping from SCC id to corresponding SCC instance. This is populated
@@ -1043,11 +1043,68 @@ def parse_parallel(self, sequential_states: list[State], parallel_states: list[S
10431043 as an optimization to parallelize only those parts of the code that can be
10441044 parallelized efficiently.
10451045 """
1046+ parallel_parsed_states , parallel_parsed_states_set = self .parse_files_threaded_raw (
1047+ sequential_states , parallel_states
1048+ )
1049+
1050+ for state in parallel_parsed_states :
1051+ # New parser returns serialized ASTs. Deserialize full trees only if not using
1052+ # parallel workers.
1053+ with state .wrap_context ():
1054+ assert state .tree is not None
1055+ raw_data = state .tree .raw_data
1056+ if raw_data is not None :
1057+ # Apply inline mypy config before deserialization, since
1058+ # some options (e.g. implicit_optional) affect deserialization
1059+ state .source_hash = raw_data .source_hash
1060+ state .apply_inline_configuration (raw_data .mypy_comments )
1061+ state .tree = load_from_raw (
1062+ state .xpath ,
1063+ state .id ,
1064+ raw_data ,
1065+ self .errors ,
1066+ state .options ,
1067+ imports_only = bool (self .workers ),
1068+ )
1069+ if self .errors .is_blockers ():
1070+ self .log ("Bailing due to parse errors" )
1071+ self .errors .raise_error ()
1072+
1073+ for state in parallel_states :
1074+ assert state .tree is not None
1075+ if state in parallel_parsed_states_set :
1076+ if state .tree .raw_data is not None :
1077+ # source_hash was already extracted above, but raw_data
1078+ # may have been preserved for workers (imports_only=True).
1079+ pass
1080+ elif state .source_hash is None :
1081+ # At least namespace packages may not have source.
1082+ state .get_source ()
1083+ state .size_hint = os .path .getsize (state .xpath )
1084+ state .early_errors = list (self .errors .error_info_map .get (state .xpath , []))
1085+ state .semantic_analysis_pass1 ()
1086+ self .ast_cache [state .id ] = (state .tree , state .early_errors , state .source_hash )
1087+ self .modules [state .id ] = state .tree
1088+ if state .tree .raw_data is not None :
1089+ state .size_hint = len (state .tree .raw_data .defs ) + MIN_SIZE_HINT
1090+ state .check_blockers ()
1091+ state .setup_errors ()
1092+
1093+ def parse_files_threaded_raw (
1094+ self , sequential_states : list [State ], parallel_states : list [State ]
1095+ ) -> tuple [list [State ], set [State ]]:
1096+ """Parse files using a thread pool.
1097+
1098+ Also parse sequential states while waiting for the parallel results.
1099+ Trees from the new parser are left in raw (serialized) form.
1100+
1101+ Return (list, set) of states that were actually parsed (not cached).
1102+ """
10461103 futures = []
10471104 # Use both list and a set to have more predictable order of errors,
10481105 # while also not sacrificing performance.
1049- parallel_parsed_states = []
1050- parallel_parsed_states_set = set ()
1106+ parallel_parsed_states : list [ State ] = []
1107+ parallel_parsed_states_set : set [ State ] = set ()
10511108 # Use at least --num-workers if specified by user.
10521109 available_threads = max (get_available_threads (), self .options .num_workers )
10531110 # Overhead from trying to parallelize (small) blocking portion of
@@ -1056,56 +1113,27 @@ def parse_parallel(self, sequential_states: list[State], parallel_states: list[S
10561113 with ThreadPoolExecutor (max_workers = min (available_threads , 8 )) as executor :
10571114 for state in parallel_states :
10581115 state .needs_parse = False
1059- # New parser reads source from file directly, we do this only for
1060- # the side effect of parsing inline mypy configurations.
1061- state .get_source ()
10621116 if state .id not in self .ast_cache :
10631117 self .log (f"Parsing { state .xpath } ({ state .id } )" )
10641118 ignore_errors = state .ignore_all or state .options .ignore_errors
10651119 if ignore_errors :
10661120 self .errors .ignored_files .add (state .xpath )
1067- futures .append (executor .submit (state .parse_file_inner , state . source or "" ))
1121+ futures .append (executor .submit (state .parse_file_inner , "" ))
10681122 parallel_parsed_states .append (state )
10691123 parallel_parsed_states_set .add (state )
10701124 else :
10711125 self .log (f"Using cached AST for { state .xpath } ({ state .id } )" )
1072- state .tree , state .early_errors = self .ast_cache [state .id ]
1126+ state .tree , state .early_errors , source_hash = self .ast_cache [state .id ]
1127+ state .source_hash = source_hash
10731128
10741129 # Parse sequential before waiting on parallel.
10751130 for state in sequential_states :
10761131 state .parse_file ()
10771132
10781133 for fut in wait (futures ).done :
10791134 fut .result ()
1080- for state in parallel_parsed_states :
1081- # New parser returns serialized trees that need to be de-serialized.
1082- with state .wrap_context ():
1083- assert state .tree is not None
1084- if state .tree .raw_data :
1085- state .tree = load_from_raw (
1086- state .xpath ,
1087- state .id ,
1088- state .tree .raw_data ,
1089- self .errors ,
1090- state .options ,
1091- imports_only = bool (self .workers ),
1092- )
1093- if self .errors .is_blockers ():
1094- self .log ("Bailing due to parse errors" )
1095- self .errors .raise_error ()
10961135
1097- for state in parallel_states :
1098- assert state .tree is not None
1099- if state in parallel_parsed_states_set :
1100- state .early_errors = list (self .errors .error_info_map .get (state .xpath , []))
1101- state .semantic_analysis_pass1 ()
1102- self .ast_cache [state .id ] = (state .tree , state .early_errors )
1103- self .modules [state .id ] = state .tree
1104- assert state .tree is not None
1105- if state .tree .raw_data is not None :
1106- state .size_hint = len (state .tree .raw_data .defs ) + MIN_SIZE_HINT
1107- state .check_blockers ()
1108- state .setup_errors ()
1136+ return parallel_parsed_states , parallel_parsed_states_set
11091137
11101138 def post_parse_all (self , states : list [State ]) -> None :
11111139 for state in states :
@@ -3134,7 +3162,6 @@ def get_source(self) -> str:
31343162 self .source_hash = compute_hash (source )
31353163
31363164 self .parse_inline_configuration (source )
3137- self .check_for_invalid_options ()
31383165
31393166 self .size_hint = len (source ) + MIN_SIZE_HINT
31403167 self .time_spent_us += time_spent_us (t0 )
@@ -3159,7 +3186,10 @@ def parse_file(self, *, temporary: bool = False, raw_data: FileRawData | None =
31593186 # The file was already parsed.
31603187 return
31613188
3162- source = self .get_source ()
3189+ if raw_data is None :
3190+ source = self .get_source ()
3191+ else :
3192+ source = ""
31633193 manager = self .manager
31643194 # Can we reuse a previously parsed AST? This avoids redundant work in daemon.
31653195 if self .id not in manager .ast_cache :
@@ -3169,6 +3199,12 @@ def parse_file(self, *, temporary: bool = False, raw_data: FileRawData | None =
31693199 self .manager .errors .ignored_files .add (self .xpath )
31703200 with self .wrap_context ():
31713201 manager .errors .set_file (self .xpath , self .id , options = self .options )
3202+ if raw_data is not None :
3203+ # Apply inline mypy config before deserialization, since
3204+ # some options (e.g. implicit_optional) affect how the
3205+ # AST is built during deserialization.
3206+ self .source_hash = raw_data .source_hash
3207+ self .apply_inline_configuration (raw_data .mypy_comments )
31723208 self .parse_file_inner (source , raw_data )
31733209 assert self .tree is not None
31743210 # New parser returns serialized trees that need to be de-serialized.
@@ -3193,14 +3229,15 @@ def parse_file(self, *, temporary: bool = False, raw_data: FileRawData | None =
31933229 else :
31943230 # Reuse a cached AST
31953231 manager .log (f"Using cached AST for { self .xpath } ({ self .id } )" )
3196- self .tree , self .early_errors = manager .ast_cache [self .id ]
3232+ self .tree , self .early_errors , source_hash = manager .ast_cache [self .id ]
3233+ self .source_hash = source_hash
31973234
31983235 assert self .tree is not None
31993236 if not temporary :
32003237 manager .modules [self .id ] = self .tree
32013238 self .check_blockers ()
32023239
3203- manager .ast_cache [self .id ] = (self .tree , self .early_errors )
3240+ manager .ast_cache [self .id ] = (self .tree , self .early_errors , self . source_hash )
32043241 assert self .tree is not None
32053242 if self .tree .raw_data is not None :
32063243 # Size of serialized tree is a better proxy for file complexity than
@@ -3221,12 +3258,17 @@ def setup_errors(self) -> None:
32213258 def parse_inline_configuration (self , source : str ) -> None :
32223259 """Check for inline mypy: options directive and parse them."""
32233260 flags = get_mypy_comments (source )
3261+ self .apply_inline_configuration (flags )
3262+
3263+ def apply_inline_configuration (self , flags : list [tuple [int , str ]] | None ) -> None :
3264+ """Apply inline mypy configuration comments and check for invalid options."""
32243265 if flags :
32253266 changes , config_errors = parse_mypy_comments (flags , self .options )
32263267 self .options = self .options .apply_changes (changes )
32273268 self .manager .errors .set_file (self .xpath , self .id , self .options )
32283269 for lineno , error in config_errors :
32293270 self .manager .error (lineno , error )
3271+ self .check_for_invalid_options ()
32303272
32313273 def check_for_invalid_options (self ) -> None :
32323274 if self .options .mypyc and not self .options .strict_bytes :
@@ -3714,19 +3756,6 @@ def find_module_and_diagnose(
37143756
37153757 ignore_missing_imports = options .ignore_missing_imports
37163758
3717- # Don't honor a global (not per-module) ignore_missing_imports
3718- # setting for modules that used to have bundled stubs, as
3719- # otherwise updating mypy can silently result in new false
3720- # negatives. (Unless there are stubs, but they are incomplete.)
3721- global_ignore_missing_imports = manager .options .ignore_missing_imports
3722- if (
3723- is_module_from_legacy_bundled_package (id )
3724- and global_ignore_missing_imports
3725- and not options .ignore_missing_imports_per_module
3726- and result is ModuleNotFoundReason .APPROVED_STUBS_NOT_INSTALLED
3727- ):
3728- ignore_missing_imports = False
3729-
37303759 if skip_diagnose :
37313760 raise ModuleNotFound
37323761 if caller_state :
0 commit comments