| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """Cache Protocol and built-in implementations for Python interpreter discovery."""
- from __future__ import annotations
- import json
- import logging
- from contextlib import contextmanager, suppress
- from hashlib import sha256
- from typing import TYPE_CHECKING, Final, Protocol, runtime_checkable
- if TYPE_CHECKING:
- from collections.abc import Generator
- from pathlib import Path
- _LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
- @runtime_checkable
- class ContentStore(Protocol):
- """A store for reading and writing cached content."""
- def exists(self) -> bool:
- """Return whether the cached content exists."""
- ...
- def read(self) -> dict | None:
- """Read the cached content, or ``None`` if unavailable or corrupt."""
- ...
- def write(self, content: dict) -> None:
- """
- Persist *content* to the store.
- :param content: interpreter metadata to cache.
- """
- ...
- def remove(self) -> None:
- """Delete the cached content."""
- ...
- @contextmanager
- def locked(self) -> Generator[None]:
- """Context manager that acquires an exclusive lock on this store."""
- ...
- @runtime_checkable
- class PyInfoCache(Protocol):
- """Cache interface for Python interpreter information."""
- def py_info(self, path: Path) -> ContentStore:
- """
- Return the content store for the interpreter at *path*.
- :param path: absolute path to a Python executable.
- """
- ...
- def py_info_clear(self) -> None:
- """Remove all cached interpreter information."""
- ...
- class DiskContentStore:
- """JSON file-based content store with file locking."""
- def __init__(self, folder: Path, key: str) -> None:
- self._folder = folder
- self._key = key
- @property
- def _file(self) -> Path:
- return self._folder / f"{self._key}.json"
- def exists(self) -> bool:
- return self._file.exists()
- def read(self) -> dict | None:
- data, bad_format = None, False
- try:
- data = json.loads(self._file.read_text(encoding="utf-8"))
- except ValueError:
- bad_format = True
- except OSError:
- _LOGGER.debug("failed to read %s", self._file, exc_info=True)
- else:
- _LOGGER.debug("got python info from %s", self._file)
- return data
- if bad_format:
- with suppress(OSError):
- self.remove()
- return None
- def write(self, content: dict) -> None:
- self._folder.mkdir(parents=True, exist_ok=True)
- self._file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8")
- _LOGGER.debug("wrote python info at %s", self._file)
- def remove(self) -> None:
- with suppress(OSError):
- self._file.unlink()
- _LOGGER.debug("removed python info at %s", self._file)
- @contextmanager
- def locked(self) -> Generator[None]:
- from filelock import FileLock # noqa: PLC0415
- lock_path = self._folder / f"{self._key}.lock"
- lock_path.parent.mkdir(parents=True, exist_ok=True)
- with FileLock(str(lock_path)):
- yield
- class DiskCache:
- """
- File-system based Python interpreter info cache (``<root>/py_info/4/<sha256>.json``).
- :param root: root directory for the on-disk cache.
- """
- def __init__(self, root: Path) -> None:
- self._root = root
- @property
- def _py_info_dir(self) -> Path:
- return self._root / "py_info" / "4"
- def py_info(self, path: Path) -> DiskContentStore:
- """
- Return the content store for the interpreter at *path*.
- :param path: absolute path to a Python executable.
- """
- key = sha256(str(path).encode("utf-8")).hexdigest()
- return DiskContentStore(self._py_info_dir, key)
- def py_info_clear(self) -> None:
- """Remove all cached interpreter information."""
- folder = self._py_info_dir
- if folder.exists():
- for entry in folder.iterdir():
- if entry.suffix == ".json":
- with suppress(OSError):
- entry.unlink()
- class NoOpContentStore(ContentStore):
- """Content store that does nothing -- implements ContentStore protocol."""
- def exists(self) -> bool: # noqa: PLR6301
- return False
- def read(self) -> dict | None: # noqa: PLR6301
- return None
- def write(self, content: dict) -> None:
- pass
- def remove(self) -> None:
- pass
- @contextmanager
- def locked(self) -> Generator[None]: # noqa: PLR6301
- yield
- class NoOpCache(PyInfoCache):
- """Cache that does nothing -- implements PyInfoCache protocol."""
- def py_info(self, path: Path) -> NoOpContentStore: # noqa: ARG002, PLR6301
- return NoOpContentStore()
- def py_info_clear(self) -> None:
- pass
- __all__ = [
- "ContentStore",
- "DiskCache",
- "DiskContentStore",
- "NoOpCache",
- "NoOpContentStore",
- "PyInfoCache",
- ]
|