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
6 changes: 5 additions & 1 deletion src/python_discovery/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ def locked(self) -> Generator[None]:


class DiskCache:
"""File-system based Python interpreter info cache (``<root>/py_info/4/<sha256>.json``)."""
"""
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
Expand Down
59 changes: 51 additions & 8 deletions src/python_discovery/_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,11 @@ def _try_posix_fallback_executable(self, base_executable: str) -> str | None:
return None # in this case we just can't tell easily without poking around FS and calling them, bail

def install_path(self, key: str) -> str:
"""Return the relative installation path for a given installation scheme *key*."""
"""
Return the relative installation path for a given installation scheme *key*.

:param key: sysconfig installation scheme key (e.g. ``"scripts"``, ``"purelib"``).
"""
result = self.distutils_install.get(key)
if result is None: # pragma: >=3.11 cover # distutils is empty when "venv" scheme is available
# set prefixes to empty => result is relative from cwd
Expand Down Expand Up @@ -308,7 +312,13 @@ def is_venv(self) -> bool:
return self.base_prefix is not None

def sysconfig_path(self, key: str, config_var: dict[str, str] | None = None, sep: str = os.sep) -> str:
"""Return the sysconfig install path for a scheme *key*, optionally substituting config variables."""
"""
Return the sysconfig install path for a scheme *key*, optionally substituting config variables.

:param key: sysconfig path key (e.g. ``"purelib"``, ``"include"``).
:param config_var: replacement mapping for sysconfig variables; when ``None`` uses the interpreter's own values.
:param sep: path separator to use in the result.
"""
pattern = self.sysconfig_paths.get(key)
if pattern is None:
return ""
Expand Down Expand Up @@ -406,14 +416,23 @@ def spec(self) -> str:

@classmethod
def clear_cache(cls, cache: PyInfoCache) -> None:
"""Clear all cached interpreter information from *cache*."""
"""
Clear all cached interpreter information from *cache*.

:param cache: the cache store to clear.
"""
from ._cached_py_info import clear # noqa: PLC0415

clear(cache)
cls._cache_exe_discovery.clear()

def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # noqa: PLR0911
"""Check if a given specification can be satisfied by this python interpreter instance."""
"""
Check if a given specification can be satisfied by this python interpreter instance.

:param spec: the specification to check against.
:param impl_must_match: when ``True``, the implementation name must match exactly.
"""
if spec.path and not self._satisfies_path(spec):
return False
if impl_must_match and not self._satisfies_implementation(spec):
Expand Down Expand Up @@ -462,7 +481,11 @@ def _satisfies_version_specifier(self, spec: PythonSpec) -> bool:

@classmethod
def current(cls, cache: PyInfoCache | None = None) -> PythonInfo:
"""Locate the current host interpreter information."""
"""
Locate the current host interpreter information.

:param cache: interpreter metadata cache; when ``None`` results are not cached.
"""
if cls._current is None:
result = cls.from_exe(sys.executable, cache, raise_on_error=True, resolve_to_host=False)
if result is None:
Expand All @@ -473,7 +496,11 @@ def current(cls, cache: PyInfoCache | None = None) -> PythonInfo:

@classmethod
def current_system(cls, cache: PyInfoCache | None = None) -> PythonInfo:
"""Locate the current system interpreter information, resolving through any virtualenv layers."""
"""
Locate the current system interpreter information, resolving through any virtualenv layers.

:param cache: interpreter metadata cache; when ``None`` results are not cached.
"""
if cls._current_system is None:
result = cls.from_exe(sys.executable, cache, raise_on_error=True, resolve_to_host=True)
if result is None:
Expand Down Expand Up @@ -504,7 +531,16 @@ def from_exe( # noqa: PLR0913
resolve_to_host: bool = True,
env: Mapping[str, str] | None = None,
) -> PythonInfo | None:
"""Get the python information for a given executable path."""
"""
Get the python information for a given executable path.

:param exe: path to the Python executable.
:param cache: interpreter metadata cache; when ``None`` results are not cached.
:param raise_on_error: raise on failure instead of returning ``None``.
:param ignore_cache: bypass the cache and re-query the interpreter.
:param resolve_to_host: resolve through virtualenv layers to the system interpreter.
:param env: environment mapping; defaults to :data:`os.environ`.
"""
from ._cached_py_info import from_exe # noqa: PLC0415

env = os.environ if env is None else env
Expand Down Expand Up @@ -583,7 +619,14 @@ def discover_exe(
exact: bool = True,
env: Mapping[str, str] | None = None,
) -> PythonInfo:
"""Discover a matching Python executable under a given *prefix* directory."""
"""
Discover a matching Python executable under a given *prefix* directory.

:param cache: interpreter metadata cache.
:param prefix: directory prefix to search under.
:param exact: when ``True``, require an exact version match.
:param env: environment mapping; defaults to :data:`os.environ`.
"""
key = prefix, exact
if key in self._cache_exe_discovery and prefix:
_LOGGER.debug("discover exe from cache %s - exact %s: %r", prefix, exact, self._cache_exe_discovery[key])
Expand Down
34 changes: 30 additions & 4 deletions src/python_discovery/_py_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,20 @@ def _parse_specifier(string_spec: str) -> PythonSpec | None:


class PythonSpec:
"""Contains specification about a Python Interpreter."""
"""
Contains specification about a Python Interpreter.

:param str_spec: the raw specification string as provided by the caller.
:param implementation: interpreter implementation name (e.g. ``"cpython"``, ``"pypy"``), or ``None`` for any.
:param major: required major version, or ``None`` for any.
:param minor: required minor version, or ``None`` for any.
:param micro: required micro (patch) version, or ``None`` for any.
:param architecture: required pointer-size bitness (``32`` or ``64``), or ``None`` for any.
:param path: filesystem path to a specific interpreter, or ``None``.
:param free_threaded: whether a free-threaded build is required, or ``None`` for any.
:param machine: required ISA (e.g. ``"arm64"``), or ``None`` for any.
:param version_specifier: PEP 440 version constraints, or ``None``.
"""

def __init__( # noqa: PLR0913, PLR0917
self,
Expand Down Expand Up @@ -128,7 +141,12 @@ def __init__( # noqa: PLR0913, PLR0917

@classmethod
def from_string_spec(cls, string_spec: str) -> PythonSpec:
"""Parse a string specification into a PythonSpec."""
"""
Parse a string specification into a :class:`PythonSpec`.

:param string_spec: an interpreter spec — an absolute path, a version string, an implementation prefix,
or a PEP 440 specifier.
"""
if pathlib.Path(string_spec).is_absolute():
return cls(string_spec, None, None, None, None, None, string_spec)
if result := _parse_spec_pattern(string_spec):
Expand All @@ -138,7 +156,11 @@ def from_string_spec(cls, string_spec: str) -> PythonSpec:
return cls(string_spec, None, None, None, None, None, string_spec)

def generate_re(self, *, windows: bool) -> re.Pattern:
"""Generate a regular expression for matching against a filename."""
"""
Generate a regular expression for matching interpreter filenames.

:param windows: if ``True``, require a ``.exe`` suffix.
"""
version = r"{}(\.{}(\.{})?)?".format(
*(r"\d+" if v is None else v for v in (self.major, self.minor, self.micro)),
)
Expand Down Expand Up @@ -189,7 +211,11 @@ def _get_required_precision(item: SimpleSpecifier) -> int | None:
return None

def satisfies(self, spec: PythonSpec) -> bool: # noqa: PLR0911
"""Check if this spec is compatible with the given *spec* (e.g. PEP-514 on Windows)."""
"""
Check if this spec is compatible with the given *spec* (e.g. PEP-514 on Windows).

:param spec: the requirement to check against.
"""
if spec.is_abs and self.is_abs and self.path != spec.path:
return False
if (
Expand Down
30 changes: 27 additions & 3 deletions src/python_discovery/_specifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@

@dataclass(**_DC_KW)
class SimpleVersion:
"""Simple PEP 440-like version parser using only standard library."""
"""
Simple PEP 440-like version parser using only standard library.

:param version_str: the original version string.
:param major: major version number.
:param minor: minor version number.
:param micro: micro (patch) version number.
:param pre_type: pre-release label (``"a"``, ``"b"``, or ``"rc"``), or ``None``.
:param pre_num: pre-release sequence number, or ``None``.
:param release: the ``(major, minor, micro)`` tuple.
"""

version_str: str
major: int
Expand Down Expand Up @@ -117,7 +127,16 @@ def __repr__(self) -> str:

@dataclass(**_DC_KW)
class SimpleSpecifier:
"""Simple PEP 440-like version specifier using only standard library."""
"""
Simple PEP 440-like version specifier using only standard library.

:param spec_str: the original specifier string (e.g. ``>=3.10``).
:param operator: the comparison operator (``==``, ``>=``, ``<``, etc.).
:param version_str: the version portion of the specifier, without the operator.
:param is_wildcard: ``True`` if the specifier uses a wildcard suffix (``.*``).
:param wildcard_precision: number of version components before the wildcard, or ``None``.
:param version: the parsed version, or ``None`` if parsing failed.
"""

spec_str: str
operator: str
Expand Down Expand Up @@ -230,7 +249,12 @@ def __repr__(self) -> str:

@dataclass(**_DC_KW)
class SimpleSpecifierSet:
"""Simple PEP 440-like specifier set using only standard library."""
"""
Simple PEP 440-like specifier set using only standard library.

:param specifiers_str: the original comma-separated specifier string.
:param specifiers: the parsed individual specifiers.
"""

specifiers_str: str
specifiers: tuple[SimpleSpecifier, ...]
Expand Down