# -*- coding=utf-8 -*- from __future__ import absolute_import, unicode_literals import abc import operator from collections import defaultdict from pipenv.vendor import attr import six from ..compat import fs_str from ..environment import MYPY_RUNNING from ..exceptions import InvalidPythonVersion from ..utils import ( KNOWN_EXTS, Sequence, expand_paths, looks_like_python, path_is_known_executable, ) if MYPY_RUNNING: from .path import PathEntry from .python import PythonVersion from typing import ( Optional, Union, Any, Dict, Iterator, List, DefaultDict, Generator, Tuple, TypeVar, Type, ) from ..compat import Path # noqa BaseFinderType = TypeVar("BaseFinderType") @attr.s(slots=True) class BasePath(object): path = attr.ib(default=None) # type: Path _children = attr.ib( default=attr.Factory(dict), cmp=False ) # type: Dict[str, PathEntry] only_python = attr.ib(default=False) # type: bool name = attr.ib(type=str) _py_version = attr.ib(default=None, cmp=False) # type: Optional[PythonVersion] _pythons = attr.ib( default=attr.Factory(defaultdict), cmp=False ) # type: DefaultDict[str, PathEntry] _is_dir = attr.ib(default=None, cmp=False) # type: Optional[bool] _is_executable = attr.ib(default=None, cmp=False) # type: Optional[bool] _is_python = attr.ib(default=None, cmp=False) # type: Optional[bool] def __str__(self): # type: () -> str return fs_str("{0}".format(self.path.as_posix())) def __lt__(self, other): # type: ("BasePath") -> bool return self.path.as_posix() < other.path.as_posix() def __lte__(self, other): # type: ("BasePath") -> bool return self.path.as_posix() <= other.path.as_posix() def __gt__(self, other): # type: ("BasePath") -> bool return self.path.as_posix() > other.path.as_posix() def __gte__(self, other): # type: ("BasePath") -> bool return self.path.as_posix() >= other.path.as_posix() def which(self, name): # type: (str) -> Optional[PathEntry] """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] + [ "{0}.{1}".format(name, ext).lower() if ext else "{0}".format(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 def __del__(self): for key in ["_is_dir", "_is_python", "_is_executable", "_py_version"]: if getattr(self, key, None): try: delattr(self, key) except Exception: print("failed deleting key: {0}".format(key)) self._children = {} for key in list(self._pythons.keys()): del self._pythons[key] self._pythons = None self._py_version = None self.path = None @property def children(self): # type: () -> Dict[str, PathEntry] if not self.is_dir: return {} return self._children @property def as_python(self): # type: () -> PythonVersion py_version = None if self.py_version: return self.py_version if not self.is_dir and self.is_python: try: from .python import PythonVersion py_version = PythonVersion.from_path( # type: ignore path=self, name=self.name ) except (ValueError, InvalidPythonVersion): pass if py_version is None: pass self.py_version = py_version return py_version # type: ignore @name.default def get_name(self): # type: () -> Optional[str] if self.path: return self.path.name return None @property def is_dir(self): # type: () -> bool if self._is_dir is None: if not self.path: ret_val = False try: ret_val = self.path.is_dir() except OSError: ret_val = False self._is_dir = ret_val return self._is_dir @is_dir.setter def is_dir(self, val): # type: (bool) -> None self._is_dir = val @is_dir.deleter def is_dir(self): # type: () -> None self._is_dir = None @property def is_executable(self): # type: () -> bool if self._is_executable is None: if not self.path: self._is_executable = False else: self._is_executable = path_is_known_executable(self.path) return self._is_executable @is_executable.setter def is_executable(self, val): # type: (bool) -> None self._is_executable = val @is_executable.deleter def is_executable(self): # type: () -> None self._is_executable = None @property def is_python(self): # type: () -> bool if self._is_python is None: if not self.path: self._is_python = False else: self._is_python = self.is_executable and ( looks_like_python(self.path.name) ) return self._is_python @is_python.setter def is_python(self, val): # type: (bool) -> None self._is_python = val @is_python.deleter def is_python(self): # type: () -> None self._is_python = None def get_py_version(self): # type: () -> Optional[PythonVersion] 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( # type: ignore 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): # type: () -> Optional[PythonVersion] if not self._py_version: py_version = self.get_py_version() self._py_version = py_version else: py_version = self._py_version return py_version @py_version.setter def py_version(self, val): # type: (Optional[PythonVersion]) -> None self._py_version = val @py_version.deleter def py_version(self): # type: () -> None self._py_version = None def _iter_pythons(self): # type: () -> 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 # type: ignore @property def pythons(self): # type: () -> DefaultDict[Union[str, Path], PathEntry] if not self._pythons: from .path import PathEntry self._pythons = defaultdict(PathEntry) for python in self._iter_pythons(): python_path = python.path.as_posix() # type: ignore self._pythons[python_path] = python return self._pythons def __iter__(self): # type: () -> Iterator for entry in self.children.values(): yield entry def __next__(self): # type: () -> Generator return next(iter(self)) def next(self): # type: () -> Generator return self.__next__() def find_all_python_versions( self, major=None, # type: Optional[Union[str, int]] minor=None, # type: Optional[int] patch=None, # type: Optional[int] pre=None, # type: Optional[bool] dev=None, # type: Optional[bool] arch=None, # type: Optional[str] name=None, # type: Optional[str] ): # type: (...) -> 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. :rtype: List[:class:`~pythonfinder.models.PathEntry`] """ call_method = "find_all_python_versions" if self.is_dir else "find_python_version" sub_finder = operator.methodcaller( 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)] version_sort = operator.attrgetter("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) def find_python_version( self, major=None, # type: Optional[Union[str, int]] minor=None, # type: Optional[int] patch=None, # type: Optional[int] pre=None, # type: Optional[bool] dev=None, # type: Optional[bool] arch=None, # type: Optional[str] name=None, # type: Optional[str] ): # type: (...) -> Optional[PathEntry] """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. """ version_matcher = operator.methodcaller( "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 # type: ignore 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=operator.itemgetter(1, 0), reverse=True) return next(iter(r[0] for r in results if r is not None), None) @six.add_metaclass(abc.ABCMeta) class BaseFinder(object): def __init__(self): #: Maps executable paths to PathEntries from .path import PathEntry self._pythons = defaultdict(PathEntry) # type: DefaultDict[str, PathEntry] self._versions = defaultdict(PathEntry) # type: Dict[Tuple, PathEntry] def get_versions(self): # type: () -> DefaultDict[Tuple, PathEntry] """Return the available versions from the finder""" raise NotImplementedError @classmethod def create(cls, *args, **kwargs): # type: (Any, Any) -> BaseFinderType raise NotImplementedError @property def version_paths(self): # type: () -> Any return self._versions.values() @property def expanded_paths(self): # type: () -> Any return (p.paths.values() for p in self.version_paths) @property def pythons(self): # type: () -> DefaultDict[str, PathEntry] return self._pythons @pythons.setter def pythons(self, value): # type: (DefaultDict[str, PathEntry]) -> None self._pythons = value