Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ class BuildManager:
Semantic analyzer, pass 2
all_types: Map {Expression: Type} from all modules (enabled by export_types)
options: Build options
missing_modules: Set of modules that could not be imported encountered so far
missing_modules: Modules that could not be imported (or intentionally skipped)
stale_modules: Set of modules that needed to be rechecked (only used by tests)
fg_deps_meta: Metadata for fine-grained dependencies caches associated with modules
fg_deps: A fine-grained dependency map
Expand Down Expand Up @@ -795,7 +795,7 @@ def __init__(
self.version_id = version_id
self.modules: dict[str, MypyFile] = {}
self.import_map: dict[str, set[str]] = {}
self.missing_modules: set[str] = set()
self.missing_modules: dict[str, int] = {}
self.fg_deps_meta: dict[str, FgDepMeta] = {}
# fg_deps holds the dependencies of every module that has been
# processed. We store this in BuildManager so that we can compute
Expand Down Expand Up @@ -2186,9 +2186,17 @@ def write_cache_meta(meta: CacheMeta, manager: BuildManager, meta_file: str) ->
"""


class SuppressionReason:
NOT_FOUND: Final = 1
SKIPPED: Final = 2


class ModuleNotFound(Exception):
"""Control flow exception to signal that a module was not found."""

def __init__(self, reason: int = SuppressionReason.NOT_FOUND) -> None:
self.reason = reason


class State:
"""The state for a module.
Expand Down Expand Up @@ -2319,9 +2327,9 @@ def new_state(
root_source,
skip_diagnose=temporary,
)
except ModuleNotFound:
except ModuleNotFound as exc:
if not temporary:
manager.missing_modules.add(id)
manager.missing_modules[id] = exc.reason
raise
if follow_imports == "silent":
ignore_all = True
Expand Down Expand Up @@ -3079,9 +3087,13 @@ def suppressed_deps_opts(self) -> bytes:
buf = WriteBuffer()
import_options = self.manager.import_options
for dep in sorted(self.suppressed):
# Using .get() is a bit defensive, but just in case we have a bug elsewhere
# (e.g. in the daemon), it is better to get a stale cache than a crash.
reason = self.manager.missing_modules.get(dep, SuppressionReason.NOT_FOUND)
if self.priorities.get(dep) != PRI_INDIRECT:
write_str_bare(buf, dep)
write_bytes_bare(buf, import_options[dep])
write_int_bare(buf, reason)
return buf.getvalue()

def write_cache(self) -> tuple[CacheMeta, str] | None:
Expand Down Expand Up @@ -3268,7 +3280,12 @@ def find_module_and_diagnose(
skipping_ancestor(manager, id, result, ancestor_for)
else:
skipping_module(manager, caller_line, caller_state, id, result)
raise ModuleNotFound
reason = SuppressionReason.SKIPPED
if options.ignore_missing_imports:
# Performance optimization: when we are ignoring imports, there is no
# difference for the caller between skipped import and actually missing one.
reason = SuppressionReason.NOT_FOUND
raise ModuleNotFound(reason=reason)
if is_silent_import_module(manager, result) and not root_source:
follow_imports = "silent"
return result, follow_imports
Expand Down Expand Up @@ -3812,7 +3829,7 @@ def load_graph(
for dep in st.ancestors + dependencies + st.suppressed:
ignored = dep in st.suppressed_set and dep not in entry_points
if ignored and dep not in added:
manager.missing_modules.add(dep)
manager.missing_modules[dep] = SuppressionReason.NOT_FOUND
# TODO: for now we skip this in the daemon as a performance optimization.
# This however creates a correctness issue, see #7777 and State.is_fresh().
if not manager.use_fine_grained_cache():
Expand Down Expand Up @@ -3876,10 +3893,10 @@ def load_graph(
# modules that are back in graph. We need to do this after the loop to cover an edge
# case where a namespace package ancestor is shared by a typed and an untyped package.
for st in graph.values():
for dep in st.suppressed:
for dep in st.suppressed.copy():
if dep in graph:
st.add_dependency(dep)
manager.missing_modules.discard(dep)
manager.missing_modules.pop(dep, None)
# Second, in the initial loop we skip indirect dependencies, so to make indirect dependencies
# behave more consistently with regular ones, we suppress them manually here (when needed).
for st in graph.values():
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ class SemanticAnalyzer(
def __init__(
self,
modules: dict[str, MypyFile],
missing_modules: set[str],
missing_modules: dict[str, int],
incomplete_namespaces: set[str],
errors: Errors,
plugin: Plugin,
Expand Down
6 changes: 4 additions & 2 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
BuildResult,
Graph,
State,
SuppressionReason,
load_graph,
process_fresh_modules,
)
Expand Down Expand Up @@ -595,7 +596,7 @@ def update_module_isolated(
sources = get_sources(manager.fscache, previous_modules, [(module, path)], followed)

if module in manager.missing_modules:
manager.missing_modules.remove(module)
del manager.missing_modules[module]

orig_module = module
orig_state = graph.get(module)
Expand Down Expand Up @@ -731,7 +732,8 @@ def delete_module(module_id: str, path: str, graph: Graph, manager: BuildManager
# If the module is removed from the build but still exists, then
# we mark it as missing so that it will get picked up by import from still.
if manager.fscache.isfile(path):
manager.missing_modules.add(module_id)
# TODO: check if there is an equivalent of #20800 for the daemon.
manager.missing_modules[module_id] = SuppressionReason.NOT_FOUND


def dedupe_modules(modules: list[tuple[str, str]]) -> list[tuple[str, str]]:
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -7993,3 +7993,22 @@ from a import b # type: ignore[attr-defined]
[out]
main:2: error: Unused "type: ignore" comment
[out2]

[case testAddedMissingModuleSkip]
# flags: --follow-imports=skip
import mod
[file mod.py.2]
[out]
main:2: error: Cannot find implementation or library stub for module named "mod"
main:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
[out2]

[case testDeletedModuleSkip]
# flags: --follow-imports=skip
import mod
[file mod.py]
[delete mod.py.2]
[out]
[out2]
main:2: error: Cannot find implementation or library stub for module named "mod"
main:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports