diff --git a/pipenv/vendor/pythonfinder/models/path.py b/pipenv/vendor/pythonfinder/models/path.py index df755fa6..d3cdd9d1 100644 --- a/pipenv/vendor/pythonfinder/models/path.py +++ b/pipenv/vendor/pythonfinder/models/path.py @@ -26,7 +26,9 @@ from ..utils import ( optional_instance_of, path_is_known_executable, unnest, - normalize_path + normalize_path, + parse_pyenv_version_order, + parse_asdf_version_order ) from .python import PythonVersion @@ -165,23 +167,26 @@ class SystemPath(object): self.path_order = new_order def _setup_asdf(self): - from .asdf import AsdfFinder + from .python import PythonFinder asdf_index = self._get_last_instance(ASDF_DATA_DIR) if not asdf_index: # we are in a virtualenv without global pyenv on the path, so we should # not write pyenv to the path here return - self.asdf_finder = AsdfFinder.create(root=ASDF_DATA_DIR, ignore_unsupported=True) + self.asdf_finder = PythonFinder.create( + root=ASDF_DATA_DIR, ignore_unsupported=True, + sort_function=parse_asdf_version_order, version_glob_path="installs/python/*") root_paths = [p for p in self.asdf_finder.roots] self._slice_in_paths(asdf_index, root_paths) self.paths.update(self.asdf_finder.roots) self._register_finder("asdf", self.asdf_finder) def _setup_pyenv(self): - from .pyenv import PyenvFinder + from .python import PythonFinder - self.pyenv_finder = PyenvFinder.create( - root=PYENV_ROOT, ignore_unsupported=self.ignore_unsupported + self.pyenv_finder = PythonFinder.create( + root=PYENV_ROOT, sort_function=parse_pyenv_version_order, + version_glob_path="versions/*", ignore_unsupported=self.ignore_unsupported ) pyenv_index = self._get_last_instance(PYENV_ROOT) if not pyenv_index: @@ -585,3 +590,29 @@ class PathEntry(BasePath): return self.is_executable and ( looks_like_python(self.path.name) ) + + +@attr.s +class VersionPath(SystemPath): + base = attr.ib(default=None, validator=optional_instance_of(Path)) + name = attr.ib(default=None) + + @classmethod + def create(cls, path, only_python=True, pythons=None, name=None): + """Accepts a path to a base python version directory. + + Generates the version listings for it""" + from .path import PathEntry + path = ensure_path(path) + path_entries = defaultdict(PathEntry) + bin_ = "{base}/bin" + if path.as_posix().endswith(Path(bin_).name): + path = path.parent + bin_dir = ensure_path(bin_.format(base=path.as_posix())) + if not name: + name = path.name + current_entry = PathEntry.create( + bin_dir, is_root=True, only_python=True, pythons=pythons, name=name + ) + path_entries[bin_dir.as_posix()] = current_entry + return cls(name=name, base=bin_dir, paths=path_entries) diff --git a/pipenv/vendor/pythonfinder/models/pyenv.py b/pipenv/vendor/pythonfinder/models/pyenv.py index cf85f57a..6f2d6422 100644 --- a/pipenv/vendor/pythonfinder/models/pyenv.py +++ b/pipenv/vendor/pythonfinder/models/pyenv.py @@ -14,8 +14,6 @@ from vistir.compat import Path from ..utils import ( ensure_path, optional_instance_of, - get_python_version, - filter_pythons, unnest, ) from .mixins import BaseFinder, BasePath diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 583dc6b3..7feee84e 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -3,23 +3,238 @@ from __future__ import absolute_import, print_function import copy import platform +import operator +import logging from collections import defaultdict import attr -from packaging.version import Version, LegacyVersion +from packaging.version import Version from packaging.version import parse as parse_version +from vistir.compat import Path -from ..environment import SYSTEM_ARCH +from ..environment import SYSTEM_ARCH, PYENV_ROOT, ASDF_DATA_DIR +from .mixins import BaseFinder, BasePath from ..utils import ( _filter_none, ensure_path, get_python_version, optional_instance_of, - ensure_path, + unnest, + is_in_path, + parse_pyenv_version_order, + parse_asdf_version_order, ) +logger = logging.getLogger(__name__) + + +@attr.s(slots=True) +class PythonFinder(BaseFinder, BasePath): + root = attr.ib(default=None, validator=optional_instance_of(Path)) + #: ignore_unsupported should come before versions, because its value is used + #: in versions's default initializer. + ignore_unsupported = attr.ib(default=True) + #: The function to use to sort version order when returning an ordered verion set + sort_function = attr.ib(default=None) + paths = attr.ib(default=attr.Factory(list)) + roots = attr.ib(default=attr.Factory(defaultdict)) + #: Glob path for python versions off of the root directory + version_glob_path = attr.ib(default="versions/*") + versions = attr.ib() + pythons = attr.ib() + + @property + def expanded_paths(self): + return ( + path for path in unnest(p for p in self.versions.values()) + if path is not None + ) + + @property + def is_pyenv(self): + return is_in_path(str(self.root), PYENV_ROOT) + + @property + def is_asdf(self): + return is_in_path(str(self.root), ASDF_DATA_DIR) + + def get_version_order(self): + version_paths = [ + p for p in self.root.glob(self.version_glob_path) + if not (p.parent.name == "envs" or p.name == "envs") + ] + versions = {v.name: v for v in version_paths} + if self.is_pyenv: + version_order = [versions[v] for v in parse_pyenv_version_order()] + elif self.is_asdf: + version_order = [versions[v] for v in parse_asdf_version_order()] + for version in version_order: + version_paths.remove(version) + if version_order: + version_order += version_paths + else: + version_order = version_paths + return version_order + + @classmethod + def version_from_bin_dir(cls, base_dir, name=None): + from .path import PathEntry + py_version = None + version_path = PathEntry.create( + path=base_dir.absolute().as_posix(), + only_python=True, + name=base_dir.parent.name, + ) + py_version = next(iter(version_path.find_all_python_versions()), None) + return py_version + + @versions.default + def get_versions(self): + from .path import PathEntry + versions = defaultdict() + bin_ = "{base}/bin" + for p in self.get_version_order(): + bin_dir = Path(bin_.format(base=p.as_posix())) + version_path = None + if bin_dir.exists(): + version_path = PathEntry.create( + path=bin_dir.absolute().as_posix(), + only_python=False, + name=p.name, + is_root=True, + ) + version = None + try: + version = PythonVersion.parse(p.name) + except ValueError: + entry = next(iter(version_path.find_all_python_versions()), None) + if not entry: + if self.ignore_unsupported: + continue + raise + else: + version = entry.py_version.as_dict() + except Exception: + if not self.ignore_unsupported: + raise + logger.warning( + "Unsupported Python version %r, ignoring...", p.name, exc_info=True + ) + continue + if not version: + continue + version_tuple = ( + version.get("major"), + version.get("minor"), + version.get("patch"), + version.get("is_prerelease"), + version.get("is_devrelease"), + version.get("is_debug"), + ) + self.roots[p] = version_path + versions[version_tuple] = version_path + self.paths.append(version_path) + return versions + + @pythons.default + def get_pythons(self): + pythons = defaultdict() + for p in self.paths: + pythons.update(p.pythons) + return pythons + + @classmethod + def create(cls, root, sort_function=None, version_glob_path=None, ignore_unsupported=True): + root = ensure_path(root) + if not version_glob_path: + version_glob_path = "versions/*" + return cls(root=root, ignore_unsupported=ignore_unsupported, + sort_function=sort_function, version_glob_path=version_glob_path) + + def find_all_python_versions( + self, + major=None, + minor=None, + patch=None, + pre=None, + dev=None, + arch=None, + name=None, + ): + """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`] + """ + + version_matcher = operator.methodcaller( + "matches", + major=major, + minor=minor, + patch=patch, + pre=pre, + dev=dev, + arch=arch, + name=name, + ) + py = operator.attrgetter("as_python") + pythons = ( + py_ver for py_ver in (py(p) for p in self.pythons.values() if p is not None) + if py_ver is not None + ) + # pythons = filter(None, [p.as_python for p in self.pythons.values()]) + matching_versions = filter(lambda py: version_matcher(py), pythons) + version_sort = operator.attrgetter("version_sort") + return sorted(matching_versions, key=version_sort, reverse=True) + + def find_python_version( + self, + major=None, + minor=None, + patch=None, + pre=None, + dev=None, + arch=None, + name=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. + """ + + version_matcher = operator.methodcaller( + "matches", + major=major, + minor=minor, + patch=patch, + pre=pre, + dev=dev, + arch=arch, + name=name, + ) + pythons = filter(None, [p.as_python for p in self.pythons.values()]) + matching_versions = filter(lambda py: version_matcher(py), pythons) + version_sort = operator.attrgetter("version_sort") + return next(iter(c for c in sorted(matching_versions, key=version_sort, reverse=True)), None) + @attr.s(slots=True) class PythonVersion(object): diff --git a/pipenv/vendor/pythonfinder/utils.py b/pipenv/vendor/pythonfinder/utils.py index fb932b10..9c71e380 100644 --- a/pipenv/vendor/pythonfinder/utils.py +++ b/pipenv/vendor/pythonfinder/utils.py @@ -7,10 +7,12 @@ import os from fnmatch import fnmatch import attr +import io import six import vistir +from .environment import PYENV_INSTALLED, PYENV_ROOT, ASDF_INSTALLED, ASDF_DATA_DIR from .exceptions import InvalidPythonVersion try: @@ -127,12 +129,6 @@ def filter_pythons(path): return filter(lambda x: path_is_python(x), path.iterdir()) -# def unnest(item): -# if isinstance(next((i for i in item), None), (list, tuple)): -# return chain(*filter(None, item)) -# return chain(filter(None, item)) - - def unnest(item): if isinstance(item, Iterable) and not isinstance(item, six.string_types): item, target = itertools.tee(item, 2) @@ -145,3 +141,30 @@ def unnest(item): yield sub else: yield el + + +def parse_pyenv_version_order(filename="version"): + version_order_file = normalize_path(os.path.join(PYENV_ROOT, filename)) + if os.path.exists(version_order_file) and os.path.isfile(version_order_file): + with io.open(version_order_file, encoding="utf-8") as fh: + contents = fh.read() + version_order = [v for v in contents.splitlines()] + return version_order + + +def parse_asdf_version_order(filename=".tool-versions"): + version_order_file = normalize_path(os.path.join("~", filename)) + if os.path.exists(version_order_file) and os.path.isfile(version_order_file): + with io.open(version_order_file, encoding="utf-8") as fh: + contents = fh.read() + python_section = next(iter( + line for line in contents.splitlines() if line.startswith("python") + ), None) + if python_section: + python_key, versions = python_section.partition() + if versions: + return versions.split() + + +def is_in_path(path, parent): + return normalize_path(str(path)).startswith(normalize_path(str(parent)))