diff --git a/news/4018.feature.rst b/news/4018.feature.rst new file mode 100644 index 0000000000..fcd6a2a91d --- /dev/null +++ b/news/4018.feature.rst @@ -0,0 +1 @@ +Added support for automatic python installs via ``asdf`` and associated ``PIPENV_DONT_USE_ASDF`` environment variable. diff --git a/pipenv/core.py b/pipenv/core.py index 3e299c8da1..74b72359df 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -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 @@ -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)): @@ -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...") ) diff --git a/pipenv/environments.py b/pipenv/environments.py index 763a61a3ea..622b76ffce 100644 --- a/pipenv/environments.py +++ b/pipenv/environments.py @@ -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. diff --git a/pipenv/pyenv.py b/pipenv/installers.py similarity index 61% rename from pipenv/pyenv.py rename to pipenv/installers.py index 941e599183..cd8e49a24a 100644 --- a/pipenv/pyenv.py +++ b/pipenv/installers.py @@ -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] @@ -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. """ @@ -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