Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASDF Support #4018

Merged
merged 6 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/4018.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for automatic python installs via ``asdf`` and associated ``PIPENV_DONT_USE_ASDF`` environment variable.
40 changes: 24 additions & 16 deletions pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
from .cmdparse import Script
from .environments import (
PIP_EXISTS_ACTION, PIPENV_CACHE_DIR, PIPENV_COLORBLIND,
PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_DONT_USE_PYENV, PIPENV_HIDE_EMOJIS,
PIPENV_MAX_SUBPROCESS, PIPENV_PYUP_API_KEY, PIPENV_RESOLVE_VCS,
PIPENV_SHELL_FANCY, PIPENV_SKIP_VALIDATION, PIPENV_YES,
PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_DONT_USE_PYENV, PIPENV_DONT_USE_ASDF,
PIPENV_HIDE_EMOJIS, PIPENV_MAX_SUBPROCESS, PIPENV_PYUP_API_KEY,
PIPENV_RESOLVE_VCS, PIPENV_SHELL_FANCY, PIPENV_SKIP_VALIDATION, PIPENV_YES,
SESSION_IS_INTERACTIVE, is_type_checking
)
from .patched import crayons
Expand Down Expand Up @@ -392,28 +392,36 @@ def abort():
),
err=True,
)
# Pyenv is installed
from .vendor.pythonfinder.environment import PYENV_INSTALLED
# check for python installers
from .vendor.pythonfinder.environment import PYENV_INSTALLED, ASDF_INSTALLED
from .installers import Pyenv, Asdf, InstallerError

# 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

if not PYENV_INSTALLED:
if not installer:
abort()
else:
if (not PIPENV_DONT_USE_PYENV) and (SESSION_IS_INTERACTIVE or PIPENV_YES):
from .pyenv import Runner, PyenvError

pyenv = Runner("pyenv")
if SESSION_IS_INTERACTIVE or PIPENV_YES:
try:
version = pyenv.find_version_to_install(python)
version = installer.find_version_to_install(python)
except ValueError:
abort()
except PyenvError as e:
except InstallerError as e:
click.echo(fix_utf8("Something went wrong…"))
click.echo(crayons.blue(e.err), err=True)
abort()
s = "{0} {1} {2}".format(
"Would you like us to install",
crayons.green("CPython {0}".format(version)),
"with pyenv?",
"with {0}?".format(installer),
)
# Prompt the user to continue…
if not (PIPENV_YES or click.confirm(s, default=True)):
Expand All @@ -424,15 +432,15 @@ def abort():
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 pyenv", bold=True),
crayons.normal(u"with {0}".format(installer), bold=True),
crayons.normal(u"(this may take a few minutes)"),
crayons.normal(fix_utf8("…"), bold=True),
)
)
with create_spinner("Installing python...") as sp:
try:
c = pyenv.install(version)
except PyenvError as e:
c = installer.install(version)
except InstallerError as e:
sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format(
"Failed...")
)
Expand Down
6 changes: 6 additions & 0 deletions pipenv/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def _is_env_truthy(name):
Default is to install Python automatically via pyenv when needed, if possible.
"""

PIPENV_DONT_USE_ASDF = bool(os.environ.get("PIPENV_DONT_USE_ASDF"))
"""If set, Pipenv does not attempt to install Python with asdf.

Default is to install Python automatically via asdf when needed, if possible.
"""

PIPENV_DOTENV_LOCATION = os.environ.get("PIPENV_DOTENV_LOCATION")
"""If set, Pipenv loads the ``.env`` file at the specified location.

Expand Down
82 changes: 64 additions & 18 deletions pipenv/pyenv.py → pipenv/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,22 @@ def matches_minor(self, other):
return (self.major, self.minor) == (other.major, other.minor)


class PyenvError(RuntimeError):
class InstallerError(RuntimeError):
def __init__(self, desc, c):
super(PyenvError, self).__init__(desc)
super(InstallerError, self).__init__(desc)
self.out = c.out
self.err = c.err


class Runner(object):
class Installer(object):

def __init__(self, pyenv):
self._cmd = pyenv
def __init__(self, cmd):
self._cmd = cmd

def _pyenv(self, *args, **kwargs):
def __str__(self):
return self._cmd

def _run(self, *args, **kwargs):
timeout = kwargs.pop('timeout', delegator.TIMEOUT)
if kwargs:
k = list(kwargs.keys())[0]
Expand All @@ -69,21 +72,16 @@ def _pyenv(self, *args, **kwargs):
c = delegator.run(args, block=False, timeout=timeout)
c.block()
if c.return_code != 0:
raise PyenvError('faild to run {0}'.format(args), c)
raise InstallerError('faild to run {0}'.format(args), c)
return c

def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install.
"""
for name in self._pyenv('install', '--list').out.splitlines():
try:
version = Version.parse(name.strip())
except ValueError:
continue
yield version
raise NotImplementedError

def find_version_to_install(self, name):
"""Find a version in pyenv from the version supplied.
"""Find a version in the installer from the version supplied.

A ValueError is raised if a matching version cannot be found.
"""
Expand All @@ -103,16 +101,64 @@ def find_version_to_install(self, name):
return best_match

def install(self, version):
"""Install the given version with pyenv.
"""Install the given version with runner implementation.

The version must be a ``Version`` instance representing a version
found in pyenv.
found in the Installer.

A ValueError is raised if the given version does not have a match in
pyenv. A PyenvError is raised if the pyenv command fails.
the runner. A InstallerError is raised if the runner command fails.
"""
c = self._pyenv(
raise NotImplementedError


class Pyenv(Installer):

def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install.
"""
for name in self._run('install', '--list').out.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.
"""
c = self._run(
'install', '-s', str(version),
timeout=PIPENV_INSTALL_TIMEOUT,
)
return c


class Asdf(Installer):

def iter_installable_versions(self):
"""Iterate through CPython versions available for asdf to install.
"""
for name in self._run('list-all', 'python').out.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=PIPENV_INSTALL_TIMEOUT,
)
return c