Skip to content

Commit 2433566

Browse files
authored
Fix skipped imports considered stale (#21639)
Fixes #21102 When the option `follow_imports = skip` is used, dependency states that are initially considered skipped might have their reason changed to not found in `load_graph`. If on the first mypy run, a given suppressed module is written to cache with its suppression reason set to skipped, and it's overridden to not found in a subsequent mypy run, then `suppressed_deps_opts` between the cache and the current run won't match and the module will be considered stale. To fix the issue, change the unconditional overwrite to `setdefault`. This also affects mypyc as the dependency being considered stale means that mypyc will needlessly recompile it instead of reading from cache. Add a unit test to confirm that the dependency output files are not overwritten with the fix.
1 parent 3179030 commit 2433566

6 files changed

Lines changed: 93 additions & 35 deletions

File tree

mypy/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4345,7 +4345,7 @@ def load_graph(
43454345
for dep in st.ancestors + dependencies + st.suppressed:
43464346
ignored = dep in st.suppressed_set and dep not in entry_points
43474347
if ignored and dep not in added:
4348-
manager.missing_modules[dep] = SuppressionReason.NOT_FOUND
4348+
manager.missing_modules.setdefault(dep, SuppressionReason.NOT_FOUND)
43494349
# TODO: for now we skip this in the daemon as a performance optimization.
43504350
# This however creates a correctness issue, see #7777 and State.is_fresh().
43514351
if not manager.use_fine_grained_cache() or manager.options.warn_unused_configs:

mypyc/build.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -328,34 +328,36 @@ def generate_c(
328328
emit_messages(options, e.messages, time.time() - t0, serious=(not e.use_stdout))
329329
sys.exit(1)
330330

331-
t1 = time.time()
332-
if result.errors:
333-
emit_messages(options, result.errors, t1 - t0)
334-
sys.exit(1)
335-
336-
if compiler_options.verbose:
337-
print(f"Parsed and typechecked in {t1 - t0:.3f}s")
338-
339-
errors = Errors(options)
340-
modules, ctext, mapper = emitmodule.compile_modules_to_c(
341-
result, compiler_options=compiler_options, errors=errors, groups=groups
342-
)
343-
t2 = time.time()
344-
emit_messages(options, errors.new_messages(), t2 - t1)
345-
if errors.num_errors:
346-
# No need to stop the build if only warnings were emitted.
347-
sys.exit(1)
348-
349-
if compiler_options.verbose:
350-
print(f"Compiled to C in {t2 - t1:.3f}s")
351-
352-
if options.mypyc_annotation_file:
353-
generate_annotated_html(options.mypyc_annotation_file, result, modules, mapper)
331+
try:
332+
t1 = time.time()
333+
if result.errors:
334+
emit_messages(options, result.errors, t1 - t0)
335+
sys.exit(1)
354336

355-
# Collect SourceDep dependencies
356-
source_deps = sorted(emitmodule.collect_source_dependencies(modules), key=lambda d: d.path)
337+
if compiler_options.verbose:
338+
print(f"Parsed and typechecked in {t1 - t0:.3f}s")
357339

358-
return ctext, "\n".join(format_modules(modules)), source_deps
340+
errors = Errors(options)
341+
modules, ctext, mapper = emitmodule.compile_modules_to_c(
342+
result, compiler_options=compiler_options, errors=errors, groups=groups
343+
)
344+
t2 = time.time()
345+
emit_messages(options, errors.new_messages(), t2 - t1)
346+
if errors.num_errors:
347+
# No need to stop the build if only warnings were emitted.
348+
sys.exit(1)
349+
350+
if compiler_options.verbose:
351+
print(f"Compiled to C in {t2 - t1:.3f}s")
352+
353+
if options.mypyc_annotation_file:
354+
generate_annotated_html(options.mypyc_annotation_file, result, modules, mapper)
355+
356+
# Collect SourceDep dependencies
357+
source_deps = sorted(emitmodule.collect_source_dependencies(modules), key=lambda d: d.path)
358+
return ctext, "\n".join(format_modules(modules)), source_deps
359+
finally:
360+
result.manager.metastore.close()
359361

360362

361363
def build_using_shared_lib(

mypyc/codegen/emitmodule.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,18 @@ def parse_and_typecheck(
210210
) -> BuildResult:
211211
assert options.strict_optional, "strict_optional must be turned on"
212212
mypyc_plugin = MypycPlugin(options, compiler_options, groups)
213-
result = build(
214-
sources=sources,
215-
options=options,
216-
alt_lib_path=alt_lib_path,
217-
fscache=fscache,
218-
extra_plugins=[mypyc_plugin],
219-
)
220-
mypyc_plugin.metastore.close()
213+
try:
214+
result = build(
215+
sources=sources,
216+
options=options,
217+
alt_lib_path=alt_lib_path,
218+
fscache=fscache,
219+
extra_plugins=[mypyc_plugin],
220+
)
221+
finally:
222+
mypyc_plugin.metastore.close()
221223
if result.errors:
224+
result.manager.metastore.close()
222225
raise CompileError(result.errors)
223226
return result
224227

mypyc/test-data/run-multimodule.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,39 @@ import native
12461246

12471247
[rechecked other_a]
12481248

1249+
-- Test that importing a skipped module does not force rechecks.
1250+
[case testIncrementalCompilationFollowImportsSkip]
1251+
from other_dep import f
1252+
assert f() == 1
1253+
1254+
[file other_dep.py]
1255+
import skipped
1256+
1257+
def f() -> int:
1258+
return 1
1259+
1260+
def g() -> int:
1261+
return 2
1262+
1263+
# Files under "skipped" are not typechecked because of the "--follow_imports = skip" option.
1264+
[file skipped/__init__.py]
1265+
x = 1
1266+
1267+
[file skipped/other_child.py]
1268+
import skipped
1269+
1270+
def child() -> int:
1271+
return 1
1272+
1273+
[file native.py.2]
1274+
from other_dep import g
1275+
assert g() == 2
1276+
1277+
[file driver.py]
1278+
import native
1279+
1280+
[rechecked native]
1281+
12491282
[case testSeparateCompilationWithUndefinedAttribute]
12501283
from other_a import A
12511284

mypyc/test/test_run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
234234
# Avoid checking modules/packages named 'unchecked', to provide a way
235235
# to test interacting with code we don't have types for.
236236
options.per_module_options["unchecked.*"] = {"follow_imports": "error"}
237+
# Avoid checking modules/packages named 'skipped', to provide a way
238+
# to test interacting with code ignored by follow_imports=skip.
239+
options.per_module_options["skipped"] = {"follow_imports": "skip"}
240+
options.per_module_options["skipped.*"] = {"follow_imports": "skip"}
237241

238242
source = build.BuildSource("native.py", "native", None)
239243
sources = [source]

test-data/unit/check-incremental.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8204,3 +8204,19 @@ reveal_type(x)
82048204
tmp/a.py:2: note: Revealed type is "builtins.int"
82058205
[out2]
82068206
tmp/a.py:2: note: Revealed type is "builtins.str"
8207+
8208+
[case testIncrementalFollowImportsSkipStubWithSkippedRuntimeDependency]
8209+
# flags: --follow-imports=skip
8210+
import pkg.foo
8211+
[file pkg/__init__.py]
8212+
8213+
[file pkg/foo.pyi]
8214+
from pkg import helper
8215+
8216+
x = 1
8217+
[file pkg/helper.py]
8218+
y = 2
8219+
[rechecked]
8220+
[stale]
8221+
[out2]
8222+
[out3]

0 commit comments

Comments
 (0)