Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 92a7858

Browse files
authored
Fix undetected submodule deletion on warm run (#20784)
Fixes #7277 This mirrors an old fix for the opposite scenario. Note that although it looks like this PR will make performance impact it should not, because a missing dependency that is present in cache means stale cache anyway. Note I also make a tiny correctness tweak in the old fix mentioned above: `follow_imports` is a per-module option so it must be cloned for the module being imported, _not_ for the importer. This still leaves a tiny correctness issue I describe in the comment, but I don't think it is possible to fix it without massive performance penalty, and it probably doesn't affect anyone anyway.
1 parent fe6fe1d commit 92a7858

2 files changed

Lines changed: 76 additions & 6 deletions

File tree

mypy/build.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,9 +2379,19 @@ def new_state(
23792379
# suppressed dependencies. Therefore, when the package with module is added,
23802380
# we need to re-calculate dependencies.
23812381
# NOTE: see comment below for why we skip this in fine-grained mode.
2382-
if exist_added_packages(suppressed, manager, options):
2382+
if exist_added_packages(suppressed, manager):
23832383
state.parse_file() # This is safe because the cache is anyway stale.
23842384
state.compute_dependencies()
2385+
# This is an inverse to the situation above. If we had an import like this:
2386+
# from pkg import mod
2387+
# and then mod was deleted, we need to force recompute dependencies, to
2388+
# decide whether we should still depend on a missing pkg.mod. Otherwise,
2389+
# the above import is indistinguishable from something like this:
2390+
# import pkg
2391+
# import pkg.mod
2392+
if exist_removed_submodules(dependencies, manager):
2393+
state.parse_file() # Same as above, the current state is stale anyway.
2394+
state.compute_dependencies()
23852395
state.size_hint = meta.size
23862396
else:
23872397
# When doing a fine-grained cache load, pretend we only
@@ -3265,7 +3275,7 @@ def find_module_and_diagnose(
32653275
raise ModuleNotFound
32663276

32673277

3268-
def exist_added_packages(suppressed: list[str], manager: BuildManager, options: Options) -> bool:
3278+
def exist_added_packages(suppressed: list[str], manager: BuildManager) -> bool:
32693279
"""Find if there are any newly added packages that were previously suppressed.
32703280
32713281
Exclude everything not in build for follow-imports=skip.
@@ -3278,13 +3288,41 @@ def exist_added_packages(suppressed: list[str], manager: BuildManager, options:
32783288
path = find_module_simple(dep, manager)
32793289
if not path:
32803290
continue
3281-
if options.follow_imports == "skip" and (
3291+
options = manager.options.clone_for_module(dep)
3292+
# Technically this is not 100% correct, since we can have:
3293+
# from pkg import mod
3294+
# with
3295+
# [mypy-pkg]
3296+
# follow-import = silent
3297+
# [mypy-pkg.mod]
3298+
# follow-imports = normal
3299+
# But such cases are extremely rare, and this allows us to avoid
3300+
# massive performance impact in much more common situations.
3301+
if options.follow_imports in ("skip", "error") and (
32823302
not path.endswith(".pyi") or options.follow_imports_for_stubs
32833303
):
32843304
continue
3285-
if "__init__.py" in path:
3286-
# It is better to have a bit lenient test, this will only slightly reduce
3287-
# performance, while having a too strict test may affect correctness.
3305+
if os.path.basename(path) in ("__init__.py", "__init__.pyi"):
3306+
return True
3307+
return False
3308+
3309+
3310+
def exist_removed_submodules(dependencies: list[str], manager: BuildManager) -> bool:
3311+
"""Find if there are any submodules of packages that are now missing.
3312+
3313+
This is conceptually an inverse of exist_added_packages().
3314+
"""
3315+
dependencies_set = set(dependencies)
3316+
for dep in dependencies:
3317+
if "." not in dep:
3318+
continue
3319+
if dep in manager.source_set.source_modules:
3320+
# We still know it is definitely a module.
3321+
continue
3322+
direct_ancestor, _ = dep.rsplit(".", maxsplit=1)
3323+
if direct_ancestor not in dependencies_set:
3324+
continue
3325+
if find_module_simple(dep, manager) is None:
32883326
return True
32893327
return False
32903328

@@ -3809,6 +3847,7 @@ def load_graph(
38093847
for dep in st.suppressed:
38103848
if dep in graph:
38113849
st.add_dependency(dep)
3850+
manager.missing_modules.discard(dep)
38123851
# Second, in the initial loop we skip indirect dependencies, so to make indirect dependencies
38133852
# behave more consistently with regular ones, we suppress them manually here (when needed).
38143853
for st in graph.values():

test-data/unit/check-incremental.test

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7962,3 +7962,34 @@ import n3 # touch
79627962
[out]
79637963
[out2]
79647964
tmp/a.py:2: error: Name "b.c.d.C" is not defined
7965+
7966+
[case testDeletedModuleFromImport]
7967+
# flags: --ignore-missing-imports
7968+
from a import b
7969+
[file a/__init__.py]
7970+
[file a/b.py]
7971+
[delete a/b.py.2]
7972+
[out]
7973+
[out2]
7974+
main:2: error: Module "a" has no attribute "b"
7975+
7976+
[case testDeletedModuleFromImport2]
7977+
# flags: --ignore-missing-imports
7978+
import a
7979+
import a.b
7980+
from a import b # no error here
7981+
[file a/__init__.py]
7982+
[file a/b.py]
7983+
[delete a/b.py.2]
7984+
[out]
7985+
[out2]
7986+
7987+
[case testDeletedModuleFromImport3]
7988+
# flags: --warn-unused-ignores
7989+
from a import b # type: ignore[attr-defined]
7990+
[file a/__init__.py]
7991+
[file a/b.py]
7992+
[delete a/b.py.2]
7993+
[out]
7994+
main:2: error: Unused "type: ignore" comment
7995+
[out2]

0 commit comments

Comments
 (0)