From ab3b404f29bed88b886fd7d66a36a5e599b72244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 16:39:28 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(cache):=20retry=20after?= =?UTF-8?q?=20cache=20I/O=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/explanation.rst | 3 +++ src/python_discovery/_cached_py_info.py | 9 ++++++++- tests/test_cached_py_info.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/explanation.rst b/docs/explanation.rst index 14ab839..537b501 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -258,6 +258,9 @@ with `filelock `_-based locking for safe co can also pass ``cache=None`` to disable caching, or implement your own backend (see :doc:`/how-to/standalone-usage`). +If cache access raises an OS error, discovery retries the interpreter query without that cache. A failed cache write +can therefore cause a second query. + Subprocess timeout behavior ---------------------------- diff --git a/src/python_discovery/_cached_py_info.py b/src/python_discovery/_cached_py_info.py index 0a28cf3..444df6d 100644 --- a/src/python_discovery/_cached_py_info.py +++ b/src/python_discovery/_cached_py_info.py @@ -62,7 +62,14 @@ def from_exe( # ruff:ignore[too-many-arguments] executable_args: str | None = None, ) -> PythonInfo | None: env = os.environ if env is None else env - result = _get_from_cache(cls, cache, exe, env, ignore_cache=ignore_cache, executable_args=executable_args) + if cache is None: + result = _get_from_cache(cls, cache, exe, env, ignore_cache=ignore_cache, executable_args=executable_args) + else: + try: + result = _get_from_cache(cls, cache, exe, env, ignore_cache=ignore_cache, executable_args=executable_args) + except OSError: + _LOGGER.debug("failed to use cache for %s", exe, exc_info=True) + result = _get_from_cache(cls, None, exe, env, ignore_cache=True, executable_args=executable_args) if isinstance(result, Exception): if raise_on_error: raise result diff --git a/tests/test_cached_py_info.py b/tests/test_cached_py_info.py index feac837..9ce5107 100644 --- a/tests/test_cached_py_info.py +++ b/tests/test_cached_py_info.py @@ -326,6 +326,30 @@ def test_from_exe_cache_entry_with_bad_content(tmp_path: Path) -> None: assert isinstance(store.read(), dict) +@pytest.mark.parametrize("suffix", [pytest.param(".json", id="entry"), pytest.param(".lock", id="lock")]) +def test_from_exe_recovers_unusable_cache_path(tmp_path: Path, suffix: str) -> None: + cache = DiskCache(tmp_path) + expected = PythonInfo.from_exe(sys.executable, cache, ignore_cache=True, resolve_to_host=False) + assert expected is not None + blocked = next(tmp_path.rglob("*.json")).with_suffix(suffix) + blocked.unlink() + blocked.mkdir() + + result = PythonInfo.from_exe(sys.executable, cache, ignore_cache=True, resolve_to_host=False) + assert result is not None + assert result.version_info == expected.version_info + + +def test_from_exe_recovers_unusable_cache_folder(tmp_path: Path) -> None: + folder = tmp_path / "py_info" / "4" + folder.parent.mkdir() + folder.write_text("occupied", encoding="utf-8") + + result = PythonInfo.from_exe(sys.executable, DiskCache(tmp_path), ignore_cache=True, resolve_to_host=False) + assert result is not None + assert result.version_info == PythonInfo().version_info + + _LAUNCHER: Final[str] = r"C:\Program Files\Python Launcher\py.exe" _ARGS: Final[str] = '-3.11 --config "settings folder\\python.json"' _SELECTED: Final[str] = r"C:\Python311\python.exe" From f935d28fb4a4d3081cf9b8d90b1501b9ac86043c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 16:42:46 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs:=20note=20cache=20recov?= =?UTF-8?q?ery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/216.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changelog/216.bugfix.rst diff --git a/docs/changelog/216.bugfix.rst b/docs/changelog/216.bugfix.rst new file mode 100644 index 0000000..820faf8 --- /dev/null +++ b/docs/changelog/216.bugfix.rst @@ -0,0 +1 @@ +Retry interpreter discovery without the cache when cache access fails.