From b7c588506208e8b9d4fb9df6f9fc0d9c7e28bbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 18:12:52 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(cache):=20hash=20filesystem?= =?UTF-8?q?=20path=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/+non-utf8-cache-path.bugfix.rst | 1 + src/python_discovery/_cache.py | 3 ++- tests/test_cache.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/+non-utf8-cache-path.bugfix.rst diff --git a/docs/changelog/+non-utf8-cache-path.bugfix.rst b/docs/changelog/+non-utf8-cache-path.bugfix.rst new file mode 100644 index 0000000..24f3e95 --- /dev/null +++ b/docs/changelog/+non-utf8-cache-path.bugfix.rst @@ -0,0 +1 @@ +Handle interpreter paths containing non-UTF-8 filesystem bytes when using DiskCache. diff --git a/src/python_discovery/_cache.py b/src/python_discovery/_cache.py index 30b8b2e..d2bd60f 100644 --- a/src/python_discovery/_cache.py +++ b/src/python_discovery/_cache.py @@ -8,6 +8,7 @@ import stat from contextlib import contextmanager, suppress from hashlib import sha256 +from os import fsencode from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Final, Protocol, runtime_checkable @@ -173,7 +174,7 @@ def py_info(self, path: Path) -> ContentStore: """ if not self._trusted_folder(): return NoOpContentStore() - key = sha256(str(path).encode("utf-8")).hexdigest() + key = sha256(fsencode(path)).hexdigest() return DiskContentStore(self._py_info_dir, key) def _trusted_folder(self) -> bool: diff --git a/tests/test_cache.py b/tests/test_cache.py index 8e38a41..1350f4b 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -3,6 +3,7 @@ import os import stat import sys +from os import fsdecode from pathlib import Path from threading import Event, Thread from typing import TYPE_CHECKING @@ -10,6 +11,7 @@ import pytest from filelock import FileLock, Timeout +from python_discovery import get_interpreter from python_discovery._cache import DiskCache, DiskContentStore, NoOpCache, NoOpContentStore if TYPE_CHECKING: @@ -118,6 +120,19 @@ def test_disk_cache_py_info(tmp_path: Path) -> None: assert isinstance(store, DiskContentStore) +@pytest.mark.skipif(os.name == "nt", reason="Windows paths do not use surrogateescape") +def test_disk_cache_non_utf8_interpreter_path(tmp_path: Path) -> None: + directory = tmp_path / fsdecode(b"interpreter-\xff") + directory.mkdir() + executable = directory / "python3" + executable.symlink_to(sys.executable) + + info = get_interpreter(str(executable), cache=DiskCache(tmp_path / "cache")) + + assert info is not None + assert info.executable == str(executable) + + def test_disk_cache_py_info_clear(tmp_path: Path) -> None: cache = DiskCache(tmp_path) store = cache.py_info(Path("/some/path"))