Fix pythonfinder

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-11-13 10:17:19 -05:00
parent e328ae24df
commit 310e0b293b
4 changed files with 284 additions and 17 deletions
+37 -6
View File
@@ -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)
-2
View File
@@ -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
+218 -3
View File
@@ -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):
+29 -6
View File
@@ -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)))