Merge branch 'bugfix/4226' of github.com:pypa/pipenv into bugfix/4226

This commit is contained in:
Dan Ryan
2020-05-18 10:57:06 -04:00
3 changed files with 86 additions and 26 deletions
+1
View File
@@ -0,0 +1 @@
Pyenv/asdf can now be used whether or not they are available on PATH. Setting PYENV_ROOT/ASDF_DIR in a Pipenv's .env allows Pipenv to install an interpreter without any shell customizations, so long as pyenv/asdf is installed.
+21 -18
View File
@@ -164,7 +164,7 @@ def load_dot_env():
err=True,
)
dotenv.load_dotenv(dotenv_file, override=True)
six.moves.reload_module(environments)
def add_to_path(p):
"""Adds a given path to the PATH."""
@@ -352,15 +352,16 @@ def find_a_system_python(line):
def ensure_python(three=None, python=None):
# Support for the PIPENV_PYTHON environment variable.
from .environments import PIPENV_PYTHON
# Runtime import is necessary due to the possibility that the environments module may have been reloaded.
from .environments import PIPENV_PYTHON, PIPENV_YES
if PIPENV_PYTHON and python is False and three is None:
python = PIPENV_PYTHON
def abort():
def abort(msg=''):
click.echo(
"You can specify specific versions of Python with:\n {0}".format(
"{0}\nYou can specify specific versions of Python with:\n{1}".format(
crayons.red(msg),
crayons.red(
"$ pipenv --python {0}".format(
os.sep.join(("path", "to", "python"))
@@ -395,21 +396,25 @@ def ensure_python(three=None, python=None):
err=True,
)
# check for python installers
from .vendor.pythonfinder.environment import PYENV_INSTALLED, ASDF_INSTALLED
from .installers import Pyenv, Asdf, InstallerError
from .installers import Pyenv, Asdf, InstallerError, InstallerNotFound
# prefer pyenv if both pyenv and asdf are installed as it's
# dedicated to python installs so probably the preferred
# method of the user for new python installs.
if PYENV_INSTALLED and not PIPENV_DONT_USE_PYENV:
installer = Pyenv("pyenv")
elif ASDF_INSTALLED and not PIPENV_DONT_USE_ASDF:
installer = Asdf("asdf")
else:
installer = None
installer = None
if not PIPENV_DONT_USE_PYENV:
try:
installer = Pyenv()
except InstallerNotFound:
pass
if installer is None and not PIPENV_DONT_USE_ASDF:
try:
installer = Asdf()
except InstallerNotFound:
pass
if not installer:
abort()
abort("Neither 'pyenv' nor 'asdf' could be found to install Python.")
else:
if SESSION_IS_INTERACTIVE or PIPENV_YES:
try:
@@ -417,9 +422,7 @@ def ensure_python(three=None, python=None):
except ValueError:
abort()
except InstallerError as e:
click.echo(fix_utf8("Something went wrong"))
click.echo(crayons.blue(e.err), err=True)
abort()
abort('Something went wrong while installing Python:\n{}'.format(e.err))
s = "{0} {1} {2}".format(
"Would you like us to install",
crayons.green("CPython {0}".format(version)),
@@ -434,7 +437,7 @@ def ensure_python(three=None, python=None):
u"{0} {1} {2} {3}{4}".format(
crayons.normal(u"Installing", bold=True),
crayons.green(u"CPython {0}".format(version), bold=True),
crayons.normal(u"with {0}".format(installer), bold=True),
crayons.normal(u"with {0}".format(installer.cmd), bold=True),
crayons.normal(u"(this may take a few minutes)"),
crayons.normal(fix_utf8(""), bold=True),
)
+64 -8
View File
@@ -1,8 +1,12 @@
import os
import operator
import re
from abc import ABCMeta, abstractmethod
from .environments import PIPENV_INSTALL_TIMEOUT
from .vendor import attr, delegator
from .utils import find_windows_executable
@attr.s
@@ -48,6 +52,10 @@ class Version(object):
return (self.major, self.minor) == (other.major, other.minor)
class InstallerNotFound(RuntimeError):
pass
class InstallerError(RuntimeError):
def __init__(self, desc, c):
super(InstallerError, self).__init__(desc)
@@ -56,29 +64,70 @@ class InstallerError(RuntimeError):
class Installer(object):
__metaclass__ = ABCMeta
def __init__(self, cmd):
self._cmd = cmd
def __init__(self):
self.cmd = self._find_installer()
super(Installer, self).__init__()
def __str__(self):
return self._cmd
@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 alows 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('~/.{}'.format(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', delegator.TIMEOUT)
if kwargs:
k = list(kwargs.keys())[0]
raise TypeError('unexpected keyword argument {0!r}'.format(k))
args = (self._cmd,) + tuple(args)
args = (self.cmd,) + tuple(args)
c = delegator.run(args, block=False, timeout=timeout)
c.block()
if c.return_code != 0:
raise InstallerError('faild to run {0}'.format(args), c)
raise InstallerError('failed to run {0}'.format(args), c)
return c
@abstractmethod
def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install.
"""
raise NotImplementedError
pass
def find_version_to_install(self, name):
"""Find a version in the installer from the version supplied.
@@ -100,6 +149,7 @@ class Installer(object):
)
return best_match
@abstractmethod
def install(self, version):
"""Install the given version with runner implementation.
@@ -109,11 +159,14 @@ class Installer(object):
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.
"""
raise NotImplementedError
pass
class Pyenv(Installer):
def _find_installer(self):
return self._find_python_installer_by_name_and_env('pyenv', 'PYENV_ROOT')
def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install.
"""
@@ -140,6 +193,9 @@ class Pyenv(Installer):
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.
"""