From cd9109fb3005819a484c5a20fff307563ab0defa Mon Sep 17 00:00:00 2001 From: King Star Date: Sun, 11 Oct 2026 01:25:20 +0800 Subject: [PATCH] Avoid false cache misses from dependency reordering Compare dependency membership and counts when validating cache freshness. Dependency order can change when graph loading suppresses and restores imports. Add an integration regression for an unchanged second run using the namespace-package reproduction. Fixes #22159. --- mypy/build.py | 5 +++- mypy/test/testbuild.py | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 mypy/test/testbuild.py diff --git a/mypy/build.py b/mypy/build.py index 09952f37aa28..59a06572cd56 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -3056,7 +3056,10 @@ def is_fresh(self) -> bool: # same for suppressed dependencies, even if the first check is OK. return ( self.meta is not None - and self.dependencies == self.meta.dependencies + # Loading and suppressing dependencies can change their iteration order. + and self.dependencies_set == set(self.meta.dependencies) + and len(self.dependencies_set) == len(self.dependencies) + and len(self.dependencies) == len(self.meta.dependencies) and ( self.options.fine_grained_incremental or self.meta.suppressed_deps_opts == self.suppressed_deps_opts() diff --git a/mypy/test/testbuild.py b/mypy/test/testbuild.py new file mode 100644 index 000000000000..329fbfc2354c --- /dev/null +++ b/mypy/test/testbuild.py @@ -0,0 +1,56 @@ +"""Tests for incremental build behavior.""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from mypy.test.helpers import Suite + + +class BuildSuite(Suite): + def test_reuses_cache_when_dependency_order_changes(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + tmp_path = Path(temporary_directory) + site = tmp_path / "site" + typed_subpackage = site / "ns" / "sub" + typed_subpackage.mkdir(parents=True) + (typed_subpackage / "__init__.py").write_text("", encoding="utf8") + (typed_subpackage / "py.typed").touch() + + (tmp_path / "a.py").write_text( + "import ns.missing\nimport os\n\ny = os.environ\n", encoding="utf8" + ) + (tmp_path / "b.py").write_text("import ns.sub\n", encoding="utf8") + (tmp_path / "e.py").write_text("import b\n", encoding="utf8") + + env = os.environ.copy() + env.pop("MYPY_CACHE_DIR", None) + env["PYTHONPATH"] = str(site) + command = [ + sys.executable, + "-m", + "mypy", + "-v", + f"--cache-dir={tmp_path / 'cache'}", + "a.py", + "e.py", + ] + + def run() -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, cwd=tmp_path, env=env, capture_output=True, text=True, check=False + ) + + first_run = run() + second_run = run() + + stale_log = "Scheduling SCC singleton (a) as inherently stale" + first_output = first_run.stdout + first_run.stderr + second_output = second_run.stdout + second_run.stderr + assert first_run.returncode == second_run.returncode == 1 + assert stale_log in first_output + assert stale_log not in second_output