418 lines
13 KiB
Python
418 lines
13 KiB
Python
# -*- 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
|