338 lines
13 KiB
Python
338 lines
13 KiB
Python
# -*- coding=utf-8 -*-
|
|
from __future__ import absolute_import, print_function
|
|
|
|
import importlib
|
|
import operator
|
|
import os
|
|
|
|
import six
|
|
from click import secho
|
|
|
|
from . import environment
|
|
from .compat import lru_cache
|
|
from .exceptions import InvalidPythonVersion
|
|
from .utils import Iterable, filter_pythons, version_re
|
|
|
|
if environment.MYPY_RUNNING:
|
|
from typing import Optional, Dict, Any, Union, List, Iterator, Text
|
|
from .models.path import Path, PathEntry
|
|
from .models.windows import WindowsFinder
|
|
from .models.path import SystemPath
|
|
|
|
STRING_TYPE = Union[str, Text, bytes]
|
|
|
|
|
|
class Finder(object):
|
|
|
|
"""
|
|
A cross-platform Finder for locating python and other executables.
|
|
|
|
Searches for python and other specified binaries starting in *path*, if supplied,
|
|
but searching the bin path of ``sys.executable`` if *system* is ``True``, and then
|
|
searching in the ``os.environ['PATH']`` if *global_search* is ``True``. When *global_search*
|
|
is ``False``, this search operation is restricted to the allowed locations of
|
|
*path* and *system*.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
path=None,
|
|
system=False,
|
|
global_search=True,
|
|
ignore_unsupported=True,
|
|
sort_by_path=False,
|
|
):
|
|
# type: (Optional[str], bool, bool, bool, bool) -> None
|
|
"""Create a new :class:`~pythonfinder.pythonfinder.Finder` instance.
|
|
|
|
:param path: A bin-directory search location, defaults to None
|
|
:param path: str, optional
|
|
:param system: Whether to include the bin-dir of ``sys.executable``, defaults to False
|
|
:param system: bool, optional
|
|
:param global_search: Whether to search the global path from os.environ, defaults to True
|
|
:param global_search: bool, optional
|
|
:param ignore_unsupported: Whether to ignore unsupported python versions, if False, an
|
|
error is raised, defaults to True
|
|
:param ignore_unsupported: bool, optional
|
|
:param bool sort_by_path: Whether to always sort by path
|
|
:returns: a :class:`~pythonfinder.pythonfinder.Finder` object.
|
|
"""
|
|
|
|
self.path_prepend = path # type: Optional[str]
|
|
self.global_search = global_search # type: bool
|
|
self.system = system # type: bool
|
|
self.sort_by_path = sort_by_path # type: bool
|
|
self.ignore_unsupported = ignore_unsupported # type: bool
|
|
self._system_path = None # type: Optional[SystemPath]
|
|
self._windows_finder = None # type: Optional[WindowsFinder]
|
|
|
|
def __hash__(self):
|
|
# type: () -> int
|
|
return hash(
|
|
(self.path_prepend, self.system, self.global_search, self.ignore_unsupported)
|
|
)
|
|
|
|
def __eq__(self, other):
|
|
# type: (Any) -> bool
|
|
return self.__hash__() == other.__hash__()
|
|
|
|
def create_system_path(self):
|
|
# type: () -> SystemPath
|
|
pyfinder_path = importlib.import_module("pythonfinder.models.path")
|
|
return pyfinder_path.SystemPath.create(
|
|
path=self.path_prepend,
|
|
system=self.system,
|
|
global_search=self.global_search,
|
|
ignore_unsupported=self.ignore_unsupported,
|
|
)
|
|
|
|
def reload_system_path(self):
|
|
# type: () -> None
|
|
"""
|
|
Rebuilds the base system path and all of the contained finders within it.
|
|
|
|
This will re-apply any changes to the environment or any version changes on the system.
|
|
"""
|
|
|
|
if self._system_path is not None:
|
|
self._system_path = self._system_path.clear_caches()
|
|
self._system_path = None
|
|
pyfinder_path = importlib.import_module("pythonfinder.models.path")
|
|
six.moves.reload_module(pyfinder_path)
|
|
self._system_path = self.create_system_path()
|
|
|
|
def rehash(self):
|
|
# type: () -> "Finder"
|
|
if not self._system_path:
|
|
self._system_path = self.create_system_path()
|
|
self.find_all_python_versions.cache_clear()
|
|
self.find_python_version.cache_clear()
|
|
if self._windows_finder is not None:
|
|
self._windows_finder = None
|
|
filter_pythons.cache_clear()
|
|
self.reload_system_path()
|
|
return self
|
|
|
|
@property
|
|
def system_path(self):
|
|
# type: () -> SystemPath
|
|
if self._system_path is None:
|
|
self._system_path = self.create_system_path()
|
|
return self._system_path
|
|
|
|
@property
|
|
def windows_finder(self):
|
|
# type: () -> Optional[WindowsFinder]
|
|
if os.name == "nt" and not self._windows_finder:
|
|
from .models import WindowsFinder
|
|
|
|
self._windows_finder = WindowsFinder()
|
|
return self._windows_finder
|
|
|
|
def which(self, exe):
|
|
# type: (str) -> Optional[PathEntry]
|
|
return self.system_path.which(exe)
|
|
|
|
@classmethod
|
|
def parse_major(
|
|
cls,
|
|
major, # type: Optional[str]
|
|
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]
|
|
):
|
|
# type: (...) -> Dict[str, Union[int, str, bool, None]]
|
|
from .models import PythonVersion
|
|
|
|
major_is_str = major and isinstance(major, six.string_types)
|
|
is_num = (
|
|
major
|
|
and major_is_str
|
|
and all(part.isdigit() for part in major.split(".")[:2])
|
|
)
|
|
major_has_arch = (
|
|
arch is None
|
|
and major
|
|
and major_is_str
|
|
and "-" in major
|
|
and major[0].isdigit()
|
|
)
|
|
name = None
|
|
if major and major_has_arch:
|
|
orig_string = "{0!s}".format(major)
|
|
major, _, arch = major.rpartition("-")
|
|
if arch:
|
|
arch = arch.lower().lstrip("x").replace("bit", "")
|
|
if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
|
|
major = orig_string
|
|
arch = None
|
|
else:
|
|
arch = "{0}bit".format(arch)
|
|
try:
|
|
version_dict = PythonVersion.parse(major)
|
|
except (ValueError, InvalidPythonVersion):
|
|
if name is None:
|
|
name = "{0!s}".format(major)
|
|
major = None
|
|
version_dict = {}
|
|
elif major and major[0].isalpha():
|
|
return {"major": None, "name": major, "arch": arch}
|
|
elif major and is_num:
|
|
match = version_re.match(major)
|
|
version_dict = match.groupdict() if match else {} # type: ignore
|
|
version_dict.update(
|
|
{
|
|
"is_prerelease": bool(version_dict.get("prerel", False)),
|
|
"is_devrelease": bool(version_dict.get("dev", False)),
|
|
}
|
|
)
|
|
else:
|
|
version_dict = {
|
|
"major": major,
|
|
"minor": minor,
|
|
"patch": patch,
|
|
"pre": pre,
|
|
"dev": dev,
|
|
"arch": arch,
|
|
}
|
|
if not version_dict.get("arch") and arch:
|
|
version_dict["arch"] = arch
|
|
version_dict["minor"] = (
|
|
int(version_dict["minor"]) if version_dict.get("minor") is not None else minor
|
|
)
|
|
version_dict["patch"] = (
|
|
int(version_dict["patch"]) if version_dict.get("patch") is not None else patch
|
|
)
|
|
version_dict["major"] = (
|
|
int(version_dict["major"]) if version_dict.get("major") is not None else major
|
|
)
|
|
if not (version_dict["major"] or version_dict.get("name")):
|
|
version_dict["major"] = major
|
|
if name:
|
|
version_dict["name"] = name
|
|
return version_dict
|
|
|
|
@lru_cache(maxsize=1024)
|
|
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]
|
|
sort_by_path=False, # type: bool
|
|
):
|
|
# type: (...) -> Optional[PathEntry]
|
|
"""
|
|
Find the python version which corresponds most closely to the version requested.
|
|
|
|
:param Union[str, int] major: The major version to look for, or the full version, or the name of the target version.
|
|
:param Optional[int] minor: The minor version. If provided, disables string-based lookups from the major version field.
|
|
:param Optional[int] patch: The patch version.
|
|
:param Optional[bool] pre: If provided, specifies whether to search pre-releases.
|
|
:param Optional[bool] dev: If provided, whether to search dev-releases.
|
|
:param Optional[str] arch: If provided, which architecture to search.
|
|
:param Optional[str] name: *Name* of the target python, e.g. ``anaconda3-5.3.0``
|
|
:param bool sort_by_path: Whether to sort by path -- default sort is by version(default: False)
|
|
:return: A new *PathEntry* pointer at a matching python version, if one can be located.
|
|
:rtype: :class:`pythonfinder.models.path.PathEntry`
|
|
"""
|
|
|
|
minor = int(minor) if minor is not None else minor
|
|
patch = int(patch) if patch is not None else patch
|
|
|
|
version_dict = {
|
|
"minor": minor,
|
|
"patch": patch,
|
|
"name": name,
|
|
"arch": arch,
|
|
} # type: Dict[str, Union[str, int, Any]]
|
|
|
|
if (
|
|
isinstance(major, six.string_types)
|
|
and pre is None
|
|
and minor is None
|
|
and dev is None
|
|
and patch is None
|
|
):
|
|
version_dict = self.parse_major(major, minor=minor, patch=patch, arch=arch)
|
|
major = version_dict["major"]
|
|
minor = version_dict.get("minor", minor) # type: ignore
|
|
patch = version_dict.get("patch", patch) # type: ignore
|
|
arch = version_dict.get("arch", arch) # type: ignore
|
|
name = version_dict.get("name", name) # type: ignore
|
|
_pre = version_dict.get("is_prerelease", pre)
|
|
pre = bool(_pre) if _pre is not None else pre
|
|
_dev = version_dict.get("is_devrelease", dev)
|
|
dev = bool(_dev) if _dev is not None else dev
|
|
if "architecture" in version_dict and isinstance(
|
|
version_dict["architecture"], six.string_types
|
|
):
|
|
arch = version_dict["architecture"] # type: ignore
|
|
if os.name == "nt" and self.windows_finder is not None:
|
|
found = self.windows_finder.find_python_version(
|
|
major=major,
|
|
minor=minor,
|
|
patch=patch,
|
|
pre=pre,
|
|
dev=dev,
|
|
arch=arch,
|
|
name=name,
|
|
)
|
|
if found:
|
|
return found
|
|
return self.system_path.find_python_version(
|
|
major=major,
|
|
minor=minor,
|
|
patch=patch,
|
|
pre=pre,
|
|
dev=dev,
|
|
arch=arch,
|
|
name=name,
|
|
sort_by_path=self.sort_by_path,
|
|
)
|
|
|
|
@lru_cache(maxsize=1024)
|
|
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]
|
|
version_sort = operator.attrgetter("as_python.version_sort")
|
|
python_version_dict = getattr(self.system_path, "python_version_dict", {})
|
|
if python_version_dict:
|
|
paths = (
|
|
path
|
|
for version in python_version_dict.values()
|
|
for path in version
|
|
if path is not None and path.as_python
|
|
)
|
|
path_list = sorted(paths, key=version_sort, reverse=True)
|
|
return path_list
|
|
versions = self.system_path.find_all_python_versions(
|
|
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
|
|
)
|
|
if not isinstance(versions, Iterable):
|
|
versions = [versions]
|
|
# This list has already been mostly sorted on windows, we don't need to reverse it again
|
|
path_list = sorted(versions, key=version_sort, reverse=True)
|
|
path_map = {} # type: Dict[str, PathEntry]
|
|
for path in path_list:
|
|
try:
|
|
resolved_path = path.path.resolve()
|
|
except OSError:
|
|
resolved_path = path.path.absolute()
|
|
if not path_map.get(resolved_path.as_posix()):
|
|
path_map[resolved_path.as_posix()] = path
|
|
return path_list
|