match_face/.venv/Lib/site-packages/pipenv/installers.py

234 lines
8.0 KiB
Python

import operator
import os
import re
import sys
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Optional
from pipenv.utils.processes import subprocess_run
from pipenv.utils.shell import find_windows_executable
@dataclass
class Version:
major: int
minor: int
patch: Optional[int] = field(default=None)
def __str__(self):
parts = [self.major, self.minor]
if self.patch is not None:
parts.append(self.patch)
return ".".join(str(p) for p in parts)
@classmethod
def parse(cls, name: str):
"""Parse an X.Y.Z, X.Y, or pre-release version string into a version tuple."""
match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?(a|b|rc)?(\d+)?$", name)
if not match:
raise ValueError(f"invalid version name {name!r}")
major = int(match.group(1))
minor = int(match.group(2))
patch = match.group(3)
if patch is not None:
patch = int(patch)
return cls(major=major, minor=minor, patch=patch)
@property
def cmpkey(self):
"""Make the version a comparable tuple.
Some old Python versions do not have a patch part, e.g., 2.7.0 is
named "2.7" in pyenv. Fix that; otherwise, `None` will fail to compare
with int.
"""
return (self.major, self.minor, self.patch or 0)
def matches_minor(self, other: "Version"):
"""Check whether this version matches the other in (major, minor)."""
return (self.major, self.minor) == (other.major, other.minor)
class InstallerNotFound(RuntimeError):
pass
class InstallerError(RuntimeError):
def __init__(self, desc, c):
super().__init__(desc)
self.out = c.stdout
self.err = c.stderr
class Installer(metaclass=ABCMeta):
def __init__(self, project):
self.cmd = self._find_installer()
self.project = project
def __str__(self):
return self.__class__.__name__
@abstractmethod
def _find_installer(self):
pass
@staticmethod
def _find_python_installer_by_name_and_env(name, env_var):
"""
Given a python installer (pyenv or asdf), try to locate the binary for that
installer.
pyenv/asdf are not always present on PATH. Both installers also support a
custom environment variable (PYENV_ROOT or ASDF_DIR) which allows them to
be installed into a non-default location (the default/suggested source
install location is in ~/.pyenv or ~/.asdf).
For systems without the installers on PATH, and with a custom location
(e.g. /opt/pyenv), Pipenv can use those installers without modifications to
PATH, if an installer's respective environment variable is present in an
environment's .env file.
This function searches for installer binaries in the following locations,
by precedence:
1. On PATH, equivalent to which(1).
2. In the "bin" subdirectory of PYENV_ROOT or ASDF_DIR, depending on the
installer.
3. In ~/.pyenv/bin or ~/.asdf/bin, depending on the installer.
"""
for candidate in (
# Look for the Python installer using the equivalent of 'which'. On
# Homebrew-installed systems, the env var may not be set, but this
# strategy will work.
find_windows_executable("", name),
# Check for explicitly set install locations (e.g. PYENV_ROOT, ASDF_DIR).
os.path.join(
os.path.expanduser(os.getenv(env_var, "/dev/null")), "bin", name
),
# Check the pyenv/asdf-recommended from-source install locations
os.path.join(os.path.expanduser(f"~/.{name}"), "bin", name),
):
if (
candidate is not None
and os.path.isfile(candidate)
and os.access(candidate, os.X_OK)
):
return candidate
raise InstallerNotFound()
def _run(self, *args, **kwargs):
timeout = kwargs.pop("timeout", 30)
shell = kwargs.pop("shell", False)
if kwargs:
k = list(kwargs.keys())[0]
raise TypeError(f"unexpected keyword argument {k!r}")
args = (self.cmd,) + tuple(args)
c = subprocess_run(args, timeout=timeout, shell=shell)
if c.returncode != 0:
raise InstallerError(f"failed to run {args}", c)
return c
@abstractmethod
def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install."""
pass
def find_version_to_install(self, name):
"""Find a version in the installer from the version supplied.
A ValueError is raised if a matching version cannot be found.
"""
version = Version.parse(name)
if version.patch is not None:
return name
try:
best_match = max(
(
inst_version
for inst_version in self.iter_installable_versions()
if inst_version.matches_minor(version)
),
key=operator.attrgetter("cmpkey"),
)
except ValueError:
raise ValueError(
f"no installable version found for {name!r}",
)
return best_match
@abstractmethod
def install(self, version):
"""Install the given version with runner implementation.
The version must be a ``Version`` instance representing a version
found in the Installer.
A ValueError is raised if the given version does not have a match in
the runner. A InstallerError is raised if the runner command fails.
"""
pass
class Pyenv(Installer):
WIN = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
def _find_installer(self):
return self._find_python_installer_by_name_and_env("pyenv", "PYENV_ROOT")
def _run(self, *args, **kwargs):
if Pyenv.WIN:
kwargs["shell"] = True
return super()._run(*args, **kwargs)
def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install."""
for name in self._run("install", "--list").stdout.splitlines():
try:
version = Version.parse(name.strip())
except ValueError:
continue
yield version
def install(self, version):
"""Install the given version with pyenv.
The version must be a ``Version`` instance representing a version
found in pyenv.
A ValueError is raised if the given version does not have a match in
pyenv. A InstallerError is raised if the pyenv command fails.
"""
args = ["install", "-s", str(version)]
if Pyenv.WIN:
# pyenv-win skips installed versions by default and does not support -s
del args[1]
return self._run(*args, timeout=self.project.s.PIPENV_INSTALL_TIMEOUT)
class Asdf(Installer):
def _find_installer(self):
return self._find_python_installer_by_name_and_env("asdf", "ASDF_DIR")
def iter_installable_versions(self):
"""Iterate through CPython versions available for asdf to install."""
for name in self._run("list-all", "python").stdout.splitlines():
try:
version = Version.parse(name.strip())
except ValueError:
continue
yield version
def install(self, version):
"""Install the given version with asdf.
The version must be a ``Version`` instance representing a version
found in asdf.
A ValueError is raised if the given version does not have a match in
asdf. A InstallerError is raised if the asdf command fails.
"""
c = self._run(
"install",
"python",
str(version),
timeout=self.project.s.PIPENV_INSTALL_TIMEOUT,
)
return c