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. diff --git a/docs/explanation.rst b/docs/explanation.rst index f40b51f..8dd1017 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -272,6 +272,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. + On POSIX, ``DiskCache`` uses a cache root only when it and both ``py_info`` directories are real directories owned by the current user or root and cannot be written by other users. Its ancestor directories must also prevent other users from replacing the cache root; a sticky shared ancestor such as ``/tmp`` is allowed. Discovery continues without disk diff --git a/src/python_discovery/_cached_py_info.py b/src/python_discovery/_cached_py_info.py index d9aff86..4776740 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 04bd5ba..c2bd15c 100644 --- a/tests/test_cached_py_info.py +++ b/tests/test_cached_py_info.py @@ -329,6 +329,27 @@ def test_from_exe_cache_entry_with_bad_content(tmp_path: Path) -> None: assert isinstance(store.read(), dict) +@pytest.mark.parametrize( + ("folder", "suffix"), + [ + pytest.param(".", ".json", id="entry"), + pytest.param("locks-v2" if os.name == "posix" else ".", ".lock", id="lock"), + ], +) +def test_from_exe_recovers_unusable_cache_path(tmp_path: Path, folder: str, 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 + entry = next(tmp_path.rglob("*.json")) + blocked = entry.parent / folder / f"{entry.stem}{suffix}" + blocked.unlink(missing_ok=True) + 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 + + _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"