@@ -724,7 +724,7 @@ class BuildManager:
724724 Semantic analyzer, pass 2
725725 all_types: Map {Expression: Type} from all modules (enabled by export_types)
726726 options: Build options
727- missing_modules: Set of modules that could not be imported encountered so far
727+ missing_modules: Modules that could not be imported (or intentionally skipped)
728728 stale_modules: Set of modules that needed to be rechecked (only used by tests)
729729 fg_deps_meta: Metadata for fine-grained dependencies caches associated with modules
730730 fg_deps: A fine-grained dependency map
@@ -785,7 +785,7 @@ def __init__(
785785 self .version_id = version_id
786786 self .modules : dict [str , MypyFile ] = {}
787787 self .import_map : dict [str , set [str ]] = {}
788- self .missing_modules : set [str ] = set ()
788+ self .missing_modules : dict [str , int ] = {}
789789 self .fg_deps_meta : dict [str , FgDepMeta ] = {}
790790 # fg_deps holds the dependencies of every module that has been
791791 # processed. We store this in BuildManager so that we can compute
@@ -2147,9 +2147,17 @@ def write_cache_meta(meta: CacheMeta, manager: BuildManager, meta_file: str) ->
21472147"""
21482148
21492149
2150+ class SuppressionReason :
2151+ NOT_FOUND : Final = 1
2152+ SKIPPED : Final = 2
2153+
2154+
21502155class ModuleNotFound (Exception ):
21512156 """Control flow exception to signal that a module was not found."""
21522157
2158+ def __init__ (self , reason : int = SuppressionReason .NOT_FOUND ) -> None :
2159+ self .reason = reason
2160+
21532161
21542162class State :
21552163 """The state for a module.
@@ -2280,9 +2288,9 @@ def new_state(
22802288 root_source ,
22812289 skip_diagnose = temporary ,
22822290 )
2283- except ModuleNotFound :
2291+ except ModuleNotFound as exc :
22842292 if not temporary :
2285- manager .missing_modules . add ( id )
2293+ manager .missing_modules [ id ] = exc . reason
22862294 raise
22872295 if follow_imports == "silent" :
22882296 ignore_all = True
@@ -3040,9 +3048,13 @@ def suppressed_deps_opts(self) -> bytes:
30403048 buf = WriteBuffer ()
30413049 import_options = self .manager .import_options
30423050 for dep in sorted (self .suppressed ):
3051+ # Using .get() is a bit defensive, but just in case we have a bug elsewhere
3052+ # (e.g. in the daemon), it is better to get a stale cache than a crash.
3053+ reason = self .manager .missing_modules .get (dep , SuppressionReason .NOT_FOUND )
30433054 if self .priorities .get (dep ) != PRI_INDIRECT :
30443055 write_str_bare (buf , dep )
30453056 write_bytes_bare (buf , import_options [dep ])
3057+ write_int_bare (buf , reason )
30463058 return buf .getvalue ()
30473059
30483060 def write_cache (self ) -> tuple [CacheMeta , str ] | None :
@@ -3229,7 +3241,12 @@ def find_module_and_diagnose(
32293241 skipping_ancestor (manager , id , result , ancestor_for )
32303242 else :
32313243 skipping_module (manager , caller_line , caller_state , id , result )
3232- raise ModuleNotFound
3244+ reason = SuppressionReason .SKIPPED
3245+ if options .ignore_missing_imports :
3246+ # Performance optimization: when we are ignoring imports, there is no
3247+ # difference for the caller between skipped import and actually missing one.
3248+ reason = SuppressionReason .NOT_FOUND
3249+ raise ModuleNotFound (reason = reason )
32333250 if is_silent_import_module (manager , result ) and not root_source :
32343251 follow_imports = "silent"
32353252 return result , follow_imports
@@ -3754,7 +3771,7 @@ def load_graph(
37543771 for dep in st .ancestors + dependencies + st .suppressed :
37553772 ignored = dep in st .suppressed_set and dep not in entry_points
37563773 if ignored and dep not in added :
3757- manager .missing_modules . add ( dep )
3774+ manager .missing_modules [ dep ] = SuppressionReason . NOT_FOUND
37583775 # TODO: for now we skip this in the daemon as a performance optimization.
37593776 # This however creates a correctness issue, see #7777 and State.is_fresh().
37603777 if not manager .use_fine_grained_cache ():
@@ -3810,10 +3827,10 @@ def load_graph(
38103827 # modules that are back in graph. We need to do this after the loop to cover an edge
38113828 # case where a namespace package ancestor is shared by a typed and an untyped package.
38123829 for st in graph .values ():
3813- for dep in st .suppressed :
3830+ for dep in st .suppressed . copy () :
38143831 if dep in graph :
38153832 st .add_dependency (dep )
3816- manager .missing_modules .discard (dep )
3833+ manager .missing_modules .pop (dep , None )
38173834 # Second, in the initial loop we skip indirect dependencies, so to make indirect dependencies
38183835 # behave more consistently with regular ones, we suppress them manually here (when needed).
38193836 for st in graph .values ():
0 commit comments