diff --git a/pipenv/core.py b/pipenv/core.py index 71452957..8f76fd19 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -351,42 +351,6 @@ def find_a_system_python(line): return python_entry -def find_python_installer(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, and only with their respective environment variables in a .env file. - This is desirable, since setting PATH in an .env file is annoying due to - the common need to reference the pre-existing PATH variable, which is not - supported in .env. - - This function searches for installer binaries using PATH, their respective - environment variables, and their respective default install locations. This - allows Pipenv to use those installers regardless of shell configuration, so - long as PYENV_ROOT or ASDF_DIR is specified in an .env file. - """ - 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 - - def ensure_python(three=None, python=None): # Runtime import is necessary due to the possibility that the environments module may have been reloaded. from .environments import PIPENV_PYTHON, PIPENV_YES @@ -394,9 +358,10 @@ def ensure_python(three=None, python=None): 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")) @@ -431,23 +396,25 @@ def ensure_python(three=None, python=None): err=True, ) # check for python installers - 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. installer = None if not PIPENV_DONT_USE_PYENV: - pyenv_path = find_python_installer('pyenv', 'PYENV_ROOT') - if pyenv_path is not None: - installer = Pyenv(pyenv_path) + try: + installer = Pyenv() + except InstallerNotFound: + pass if installer is None and not PIPENV_DONT_USE_ASDF: - asdf_path = find_python_installer('asdf', 'ASDF_DIR') - if asdf_path is not None: - installer = Asdf(asdf_path) + try: + installer = Pyenv() + 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: @@ -455,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)), diff --git a/pipenv/installers.py b/pipenv/installers.py index cd8e49a2..70bf97d5 100644 --- a/pipenv/installers.py +++ b/pipenv/installers.py @@ -1,5 +1,7 @@ import operator import re +from abc import ABCMeta, abstractmethod + from .environments import PIPENV_INSTALL_TIMEOUT from .vendor import attr, delegator @@ -48,6 +50,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,12 +62,52 @@ 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) @@ -72,13 +118,14 @@ class Installer(object): 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 +147,7 @@ class Installer(object): ) return best_match + @abstractmethod def install(self, version): """Install the given version with runner implementation. @@ -109,11 +157,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 +191,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. """