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
51 changes: 45 additions & 6 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2373,9 +2373,19 @@ def new_state(
# suppressed dependencies. Therefore, when the package with module is added,
# we need to re-calculate dependencies.
# NOTE: see comment below for why we skip this in fine-grained mode.
if exist_added_packages(suppressed, manager, options):
if exist_added_packages(suppressed, manager):
state.parse_file() # This is safe because the cache is anyway stale.
state.compute_dependencies()
# This is an inverse to the situation above. If we had an import like this:
# from pkg import mod
# and then mod was deleted, we need to force recompute dependencies, to
# decide whether we should still depend on a missing pkg.mod. Otherwise,
# the above import is indistinguishable from something like this:
# import pkg
# import pkg.mod
if exist_removed_submodules(dependencies, manager):
state.parse_file() # Same as above, the current state is stale anyway.
state.compute_dependencies()
state.size_hint = meta.size
else:
# When doing a fine-grained cache load, pretend we only
Expand Down Expand Up @@ -3242,7 +3252,7 @@ def find_module_and_diagnose(
raise ModuleNotFound


def exist_added_packages(suppressed: list[str], manager: BuildManager, options: Options) -> bool:
def exist_added_packages(suppressed: list[str], manager: BuildManager) -> bool:
"""Find if there are any newly added packages that were previously suppressed.

Exclude everything not in build for follow-imports=skip.
Expand All @@ -3255,13 +3265,41 @@ def exist_added_packages(suppressed: list[str], manager: BuildManager, options:
path = find_module_simple(dep, manager)
if not path:
continue
if options.follow_imports == "skip" and (
options = manager.options.clone_for_module(dep)
# Technically this is not 100% correct, since we can have:
# from pkg import mod
# with
# [mypy-pkg]
# follow-import = silent
# [mypy-pkg.mod]
# follow-imports = normal
# But such cases are extremely rare, and this allows us to avoid
# massive performance impact in much more common situations.
if options.follow_imports in ("skip", "error") and (
not path.endswith(".pyi") or options.follow_imports_for_stubs
):
continue
if "__init__.py" in path:
# It is better to have a bit lenient test, this will only slightly reduce
# performance, while having a too strict test may affect correctness.
if os.path.basename(path) in ("__init__.py", "__init__.pyi"):
return True
return False


def exist_removed_submodules(dependencies: list[str], manager: BuildManager) -> bool:
"""Find if there are any submodules of packages that are now missing.

This is conceptually an inverse of exist_added_packages().
"""
dependencies_set = set(dependencies)
for dep in dependencies:
if "." not in dep:
continue
if dep in manager.source_set.source_modules:
# We still know it is definitely a module.
continue
direct_ancestor, _ = dep.rsplit(".", maxsplit=1)
if direct_ancestor not in dependencies_set:
continue
if find_module_simple(dep, manager) is None:
return True
return False

Expand Down Expand Up @@ -3784,6 +3822,7 @@ def load_graph(
for dep in st.suppressed:
if dep in graph:
st.add_dependency(dep)
manager.missing_modules.discard(dep)
# 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
31 changes: 31 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -7864,3 +7864,34 @@ import n3 # touch
[out]
[out2]
tmp/a.py:2: error: Name "b.c.d.C" is not defined

[case testDeletedModuleFromImport]
# flags: --ignore-missing-imports
from a import b
[file a/__init__.py]
[file a/b.py]
[delete a/b.py.2]
[out]
[out2]
main:2: error: Module "a" has no attribute "b"

[case testDeletedModuleFromImport2]
# flags: --ignore-missing-imports
import a
import a.b
from a import b # no error here
[file a/__init__.py]
[file a/b.py]
[delete a/b.py.2]
[out]
[out2]

[case testDeletedModuleFromImport3]
# flags: --warn-unused-ignores
from a import b # type: ignore[attr-defined]
[file a/__init__.py]
[file a/b.py]
[delete a/b.py.2]
[out]
main:2: error: Unused "type: ignore" comment
[out2]