Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/+non-utf8-cache-path.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle interpreter paths containing non-UTF-8 filesystem bytes when using DiskCache.
3 changes: 2 additions & 1 deletion src/python_discovery/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import os
import stat
import sys
from os import fsdecode
from pathlib import Path
from threading import Event, Thread
from typing import TYPE_CHECKING

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:
Expand Down Expand Up @@ -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"))
Expand Down