Source code for pythonfinder.models.mixins

from __future__ import annotations

import os
from collections import defaultdict
from pathlib import Path
from typing import (
    TYPE_CHECKING,
    Any,
    Dict,
    Generator,
    Iterator,
    Optional,
)

from pydantic import BaseModel, Field, validator

from ..exceptions import InvalidPythonVersion
from ..utils import (
    KNOWN_EXTS,
    ensure_path,
    expand_paths,
    filter_pythons,
    looks_like_python,
    path_is_known_executable,
)

if TYPE_CHECKING:
    from pythonfinder.models.python import PythonVersion


[docs]class PathEntry(BaseModel): is_root: bool = Field(default=False, order=False) name: Optional[str] = None path: Optional[Path] = None children_ref: Optional[Any] = Field(default_factory=lambda: dict()) only_python: Optional[bool] = False py_version_ref: Optional[Any] = None pythons_ref: Optional[Dict[Any, Any]] = defaultdict(lambda: None) is_dir_ref: Optional[bool] = None is_executable_ref: Optional[bool] = None is_python_ref: Optional[bool] = None
[docs] class Config: validate_assignment = True arbitrary_types_allowed = True allow_mutation = True include_private_attributes = True
[docs] @validator("children", pre=True, always=True, check_fields=False) def set_children(cls, v, values, **kwargs): path = values.get("path") if path: values["name"] = path.name return v or cls()._gen_children()
def __str__(self) -> str: return f"{self.path.as_posix()}" def __lt__(self, other) -> bool: return self.path.as_posix() < other.path.as_posix() def __lte__(self, other) -> bool: return self.path.as_posix() <= other.path.as_posix() def __gt__(self, other) -> bool: return self.path.as_posix() > other.path.as_posix() def __gte__(self, other) -> bool: return self.path.as_posix() >= other.path.as_posix() def __eq__(self, other) -> bool: return self.path.as_posix() == other.path.as_posix()
[docs] def which(self, name) -> PathEntry | None: """Search in this path for an executable. :param executable: The name of an executable to search for. :type executable: str :returns: :class:`~pythonfinder.models.PathEntry` instance. """ valid_names = [name] + [ f"{name}.{ext}".lower() if ext else f"{name}".lower() for ext in KNOWN_EXTS ] children = self.children found = None if self.path is not None: found = next( ( children[(self.path / child).as_posix()] for child in valid_names if (self.path / child).as_posix() in children ), None, ) return found
@property def as_python(self) -> PythonVersion: py_version = None if self.py_version_ref: return self.py_version_ref if not self.is_dir and self.is_python: from .python import PythonVersion try: py_version = PythonVersion.from_path(path=self, name=self.name) except (ValueError, InvalidPythonVersion): pass self.py_version_ref = py_version return self.py_version_ref @property def is_dir(self) -> bool: if self.is_dir_ref is None: try: ret_val = self.path.is_dir() except OSError: ret_val = False self.is_dir_ref = ret_val return self.is_dir_ref @is_dir.setter def is_dir(self, val) -> None: self.is_dir_ref = val @is_dir.deleter def is_dir(self) -> None: self.is_dir_ref = None @property def is_executable(self) -> bool: if self.is_executable_ref is None: if not self.path: self.is_executable_ref = False else: self.is_executable_ref = path_is_known_executable(self.path) return self.is_executable_ref @is_executable.setter def is_executable(self, val) -> None: self.is_executable_ref = val @is_executable.deleter def is_executable(self) -> None: self.is_executable_ref = None @property def is_python(self) -> bool: if self.is_python_ref is None: if not self.path: self.is_python_ref = False else: self.is_python_ref = self.is_executable and ( looks_like_python(self.path.name) ) return self.is_python_ref @is_python.setter def is_python(self, val) -> None: self.is_python_ref = val @is_python.deleter def is_python(self) -> None: self.is_python_ref = None
[docs] def get_py_version(self): from ..environment import IGNORE_UNSUPPORTED if self.is_dir: return None if self.is_python: py_version = None from .python import PythonVersion try: py_version = PythonVersion.from_path(path=self, name=self.name) except (InvalidPythonVersion, ValueError): py_version = None except Exception: if not IGNORE_UNSUPPORTED: raise return py_version return None
@property def py_version(self) -> PythonVersion | None: if not self.py_version_ref: py_version = self.get_py_version() self.py_version_ref = py_version else: py_version = self.py_version_ref return py_version def _iter_pythons(self) -> Iterator: if self.is_dir: for entry in self.children.values(): if entry is None: continue elif entry.is_dir: for python in entry._iter_pythons(): yield python elif entry.is_python and entry.as_python is not None: yield entry elif self.is_python and self.as_python is not None: yield self @property def pythons(self) -> dict[str | Path, PathEntry]: if not self.pythons_ref: self.pythons_ref = defaultdict(PathEntry) for python in self._iter_pythons(): python_path = python.path.as_posix() self.pythons_ref[python_path] = python return self.pythons_ref def __iter__(self) -> Iterator: yield from self.children.values() def __next__(self) -> Generator: return next(iter(self))
[docs] def next(self) -> Generator: return self.__next__()
[docs] def find_all_python_versions( self, major: str | int | None = None, minor: int | None = None, patch: int | None = None, pre: bool | None = None, dev: bool | None = None, arch: str | None = None, name: str | None = None, ) -> list[PathEntry]: """Search for a specific python version on the path. Return all copies :param major: Major python version to search for. :type major: int :param int minor: Minor python version to search for, defaults to None :param int patch: Patch python version to search for, defaults to None :param bool pre: Search for prereleases (default None) - prioritize releases if None :param bool dev: Search for devreleases (default None) - prioritize releases if None :param str arch: Architecture to include, e.g. '64bit', defaults to None :param str name: The name of a python version, e.g. ``anaconda3-5.3.0`` :return: A list of :class:`~pythonfinder.models.PathEntry` instances matching the version requested. """ call_method = "find_all_python_versions" if self.is_dir else "find_python_version" def sub_finder(obj): return getattr(obj, call_method)(major, minor, patch, pre, dev, arch, name) if not self.is_dir: return sub_finder(self) unnested = [sub_finder(path) for path in expand_paths(self)] def version_sort(path_entry): return path_entry.as_python.version_sort unnested = [p for p in unnested if p is not None and p.as_python is not None] paths = sorted(unnested, key=version_sort, reverse=True) return list(paths)
[docs] def find_python_version( self, major: str | int | None = None, minor: int | None = None, patch: int | None = None, pre: bool | None = None, dev: bool | None = None, arch: str | None = None, name: str | None = None, ) -> PathEntry | None: """Search or self for the specified Python version and return the first match. :param major: Major version number. :type major: int :param int minor: Minor python version to search for, defaults to None :param int patch: Patch python version to search for, defaults to None :param bool pre: Search for prereleases (default None) - prioritize releases if None :param bool dev: Search for devreleases (default None) - prioritize releases if None :param str arch: Architecture to include, e.g. '64bit', defaults to None :param str name: The name of a python version, e.g. ``anaconda3-5.3.0`` :returns: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested. """ def version_matcher(py_version): return py_version.matches( major, minor, patch, pre, dev, arch, python_name=name ) if not self.is_dir: if self.is_python and self.as_python and version_matcher(self.py_version): return self matching_pythons = [ [entry, entry.as_python.version_sort] for entry in self._iter_pythons() if ( entry is not None and entry.as_python is not None and version_matcher(entry.py_version) ) ] results = sorted(matching_pythons, key=lambda r: (r[1], r[0]), reverse=True) return next(iter(r[0] for r in results if r is not None), None)
def _filter_children(self) -> Iterator[Path]: if not os.access(str(self.path), os.R_OK): return iter([]) if self.only_python: children = filter_pythons(self.path) else: children = self.path.iterdir() return children def _gen_children(self) -> Iterator: pass_name = self.name != self.path.name pass_args = {"is_root": False, "only_python": self.only_python} if pass_name: if self.name is not None and isinstance(self.name, str): pass_args["name"] = self.name elif self.path is not None and isinstance(self.path.name, str): pass_args["name"] = self.path.name if not self.is_dir: yield (self.path.as_posix(), self) elif self.is_root: for child in self._filter_children(): if self.only_python: try: entry = PathEntry.create(path=child, **pass_args) except (InvalidPythonVersion, ValueError): continue else: try: entry = PathEntry.create(path=child, **pass_args) except (InvalidPythonVersion, ValueError): continue yield (child.as_posix(), entry) return @property def children(self) -> dict[str, PathEntry]: children = getattr(self, "children_ref", {}) if not children: for child_key, child_val in self._gen_children(): children[child_key] = child_val self.children_ref = children return self.children_ref
[docs] @classmethod def create( cls, path: str | Path, is_root: bool = False, only_python: bool = False, pythons: dict[str, PythonVersion] | None = None, name: str | None = None, ) -> PathEntry: """Helper method for creating new :class:`pythonfinder.models.PathEntry` instances. :param str path: Path to the specified location. :param bool is_root: Whether this is a root from the environment PATH variable, defaults to False :param bool only_python: Whether to search only for python executables, defaults to False :param dict pythons: A dictionary of existing python objects (usually from a finder), defaults to None :param str name: Name of the python version, e.g. ``anaconda3-5.3.0`` :return: A new instance of the class. """ target = ensure_path(path) guessed_name = False if not name: guessed_name = True name = target.name creation_args = { "path": target, "is_root": is_root, "only_python": only_python, "name": name, } if pythons: creation_args["pythons"] = pythons _new = cls(**creation_args) if pythons and only_python: children = {} child_creation_args = {"is_root": False, "only_python": only_python} if not guessed_name: child_creation_args["name"] = _new.name for pth, python in pythons.items(): pth = ensure_path(pth) children[pth.as_posix()] = PathEntry( py_version=python, path=pth, **child_creation_args ) _new.children_ref = children return _new