From 524795bd5faa0119c238c68c5d9042086c8fbaab Mon Sep 17 00:00:00 2001 From: Sam Stavinoha Date: Mon, 7 Mar 2016 18:38:09 +0900 Subject: [PATCH 1/2] dont fail if the version is available If the python version is shown by `pyenv versions`, use $PYENV_VERSION to enable the version and retry the operation. Fixes #3 --- tox_pyenv.py | 133 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/tox_pyenv.py b/tox_pyenv.py index 31b40f2..a88884e 100644 --- a/tox_pyenv.py +++ b/tox_pyenv.py @@ -28,6 +28,17 @@ def tox_get_python_executable(envconfig): """ +import logging +import ntpath +import os +import re +import subprocess + +from distutils.version import LooseVersion + +import py +from tox import hookimpl as tox_hookimpl + # __about__ __title__ = 'tox-pyenv' __summary__ = ('tox plugin that makes tox use `pyenv which` ' @@ -41,13 +52,9 @@ def tox_get_python_executable(envconfig): # __about__ -import logging -import subprocess - -import py -from tox import hookimpl as tox_hookimpl - LOG = logging.getLogger(__name__) +PYTHON_VERSION_RE = re.compile(r'^(?:python|py)([\d\.]{1,8})$', + flags=re.IGNORECASE) class ToxPyenvException(Exception): @@ -65,28 +72,35 @@ class PyenvWhichFailed(ToxPyenvException): """Calling `pyenv which` failed.""" -@tox_hookimpl -def tox_get_python_executable(envconfig): - """Return a python executable for the given python base name. +class NoSuitableVersionFound(ToxPyenvException): + + """Could not a find a python version that satisfies requirement.""" + - The first plugin/hook which returns an executable path will determine it. +def _get_pyenv_known_versions(): + """Return searchable output from `pyenv versions`.""" + known_versions = _pyenv_run(['versions'])[0].split(os.linesep) + return [v.strip() for v in known_versions if v.strip()] - ``envconfig`` is the testenv configuration which contains - per-testenv configuration, notably the ``.envname`` and ``.basepython`` - setting. + +def _pyenv_run(command, **popen_kwargs): + """Run pyenv command with Popen. + + Returns the result tuple as (stdout, stderr, returncode). """ try: - # pylint: disable=no-member - pyenv = (getattr(py.path.local.sysfind('pyenv'), 'strpath', 'pyenv') - or 'pyenv') - cmd = [pyenv, 'which', envconfig.basepython] + pyenv = (getattr( + py.path.local.sysfind('pyenv'), 'strpath', 'pyenv') or 'pyenv') + cmd = [pyenv] + command pipe = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True + universal_newlines=True, + **popen_kwargs ) out, err = pipe.communicate() + out, err = out.strip(), err.strip() except OSError: err = '\'pyenv\': command not found' LOG.warning( @@ -94,18 +108,91 @@ def tox_get_python_executable(envconfig): "don't want this plugin installed either." ) else: - if pipe.poll() == 0: + returncode = pipe.poll() + if returncode == 0: return out.strip() else: if not envconfig.tox_pyenv_fallback: - raise PyenvWhichFailed(err) - LOG.debug("`%s` failed thru tox-pyenv plugin, falling back. " - "STDERR: \"%s\" | To disable this behavior, set " + cmdstr = ' '.join([str(x) for x in cmd]) + LOG.error("The command `%s` executed by the tox-pyenv plugin failed. " + "STDERR: \"%s\" STDOUT: \"%s\"", cmdstr, err, out) + raise subprocess.CalledProcessError(returncode, cmdstr, output=err) + LOG.error("`%s` failed thru tox-pyenv plugin, falling back to " + "tox's built-in behavior. " + "STDERR: \"%s\" | To disable this fallback, set " "tox_pyenv_fallback=False in your tox.ini or use " " --tox-pyenv-no-fallback on the command line.", ' '.join([str(x) for x in cmd]), err) +def _extrapolate_to_known_version(desired, known): + """Given the desired version, find an acceptable available version.""" + match = PYTHON_VERSION_RE.match(desired) + if match: + match = match.groups()[0] + if match in known: + return match + else: + matches = sorted([LooseVersion(j) for j in known + if j.startswith(match)]) + if matches: + # Select the latest. + # e.g. python2 gets 2.7.10 + # if known_versions = ['2.7.3', '2.7', '2.7.10'] + return matches[-1].vstring + raise NoSuitableVersionFound( + 'Given desired version {0}, no suitable version of python could ' + 'be matched in the list given by `pyenv versions`.'.format(desired)) + + +def _set_env_and_retry(envconfig): + # Let's be smart, and resilient to 'command not found' + # especially if we can reasonably figure out which + # version of python is desired, and that version of python + # is installed and available through pyenv. + desired_version = ntpath.basename(envconfig.basepython) + LOG.debug("tox-pyenv is now looking for the desired python " + "version (%s) through pyenv. If it is found, it will " + "be enabled and this operation retried.", desired_version) + + def _enable_and_call(_available_version): + LOG.debug('Enabling %s by setting $PYENV_VERSION to %s', + desired_version, _available_version) + _env = os.environ.copy() + _env['PYENV_VERSION'] = _available_version + return _pyenv_run( + ['which', envconfig.basepython], env=_env)[0] + + known_versions = _get_pyenv_known_versions() + + if desired_version in known_versions: + return _enable_and_call(desired_version) + else: + match = _extrapolate_to_known_version( + desired_version, known_versions) + return _enable_and_call(match) + + +@tox_hookimpl +def tox_get_python_executable(envconfig): + """Hook into tox plugins to use pyenv to find executables.""" + + try: + out, err = _pyenv_run(['which', envconfig.basepython]) + except subprocess.CalledProcessError: + try: + return _set_env_and_retry(envconfig) + except (subprocess.CalledProcessError, NoSuitableVersionFound): + if not envconfig.tox_pyenv_fallback: + raise PyenvWhichFailed(err) + LOG.debug("tox-pyenv plugin failed, falling back. " + "To disable this behavior, set " + "tox_pyenv_fallback=False in your tox.ini or use " + " --tox-pyenv-no-fallback on the command line.") + else: + return out + + def _setup_no_fallback(parser): """Add the option, --tox-pyenv-no-fallback. @@ -149,5 +236,5 @@ def _pyenv_fallback(testenv_config, value): @tox_hookimpl def tox_addoption(parser): - """Add command line option to the argparse-style parser object.""" + """Add command line options to the argparse-style parser object.""" _setup_no_fallback(parser) From 2d6fcbb024fde2d2f11f74459793f1e719b1a09c Mon Sep 17 00:00:00 2001 From: Sam Stavinoha Date: Tue, 23 Jan 2018 20:28:50 -0600 Subject: [PATCH 2/2] preemptive conflict avoidance --- setup.py | 2 +- tox_pyenv.py | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index 775fa7c..fe52dab 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ here = os.path.dirname(os.path.realpath(__file__)) with open(os.path.join(here, 'tox_pyenv.py'), 'r') as abt: marker, about, abt = '# __about__', {}, abt.read() - assert abt.count('# __about__') == 2 + assert abt.count(marker) == 2 abt = abt[abt.index(marker):abt.rindex(marker)] exec(abt, about) diff --git a/tox_pyenv.py b/tox_pyenv.py index a88884e..920c3cc 100644 --- a/tox_pyenv.py +++ b/tox_pyenv.py @@ -67,11 +67,6 @@ class PyenvMissing(ToxPyenvException, RuntimeError): """The pyenv program is not installed.""" -class PyenvWhichFailed(ToxPyenvException): - - """Calling `pyenv which` failed.""" - - class NoSuitableVersionFound(ToxPyenvException): """Could not a find a python version that satisfies requirement.""" @@ -89,9 +84,11 @@ def _pyenv_run(command, **popen_kwargs): Returns the result tuple as (stdout, stderr, returncode). """ try: - pyenv = (getattr( - py.path.local.sysfind('pyenv'), 'strpath', 'pyenv') or 'pyenv') - cmd = [pyenv] + command + pyenv_bin = getattr( + py.path.local.sysfind('pyenv'), 'strpath', 'pyenv' + ) + pyenv_bin = pyenv_bin or 'pyenv' + cmd = [pyenv_bin] + command pipe = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -102,27 +99,20 @@ def _pyenv_run(command, **popen_kwargs): out, err = pipe.communicate() out, err = out.strip(), err.strip() except OSError: - err = '\'pyenv\': command not found' LOG.warning( "pyenv doesn't seem to be installed, you probably " "don't want this plugin installed either." ) + raise else: returncode = pipe.poll() if returncode == 0: - return out.strip() + return out, err else: - if not envconfig.tox_pyenv_fallback: - cmdstr = ' '.join([str(x) for x in cmd]) - LOG.error("The command `%s` executed by the tox-pyenv plugin failed. " - "STDERR: \"%s\" STDOUT: \"%s\"", cmdstr, err, out) - raise subprocess.CalledProcessError(returncode, cmdstr, output=err) - LOG.error("`%s` failed thru tox-pyenv plugin, falling back to " - "tox's built-in behavior. " - "STDERR: \"%s\" | To disable this fallback, set " - "tox_pyenv_fallback=False in your tox.ini or use " - " --tox-pyenv-no-fallback on the command line.", - ' '.join([str(x) for x in cmd]), err) + cmdstr = ' '.join([str(x) for x in cmd]) + LOG.error("The command `%s` executed by the tox-pyenv plugin failed. " + "STDERR: \"%s\" STDOUT: \"%s\"", cmdstr, err, out) + raise subprocess.CalledProcessError(returncode, cmdstr, output=err) def _extrapolate_to_known_version(desired, known): @@ -142,7 +132,8 @@ def _extrapolate_to_known_version(desired, known): return matches[-1].vstring raise NoSuitableVersionFound( 'Given desired version {0}, no suitable version of python could ' - 'be matched in the list given by `pyenv versions`.'.format(desired)) + 'be matched in the list given by `pyenv versions`.'.format(desired) + ) def _set_env_and_retry(envconfig): @@ -179,12 +170,21 @@ def tox_get_python_executable(envconfig): try: out, err = _pyenv_run(['which', envconfig.basepython]) + except OSError: + # pyenv not installed or executable could not be found + if not envconfig.tox_pyenv_fallback: + raise + LOG.error("tox-pyenv plugin failed, falling back. " + "To disable this behavior, set " + "tox_pyenv_fallback=False in your tox.ini or use " + " --tox-pyenv-no-fallback on the command line.") + return except subprocess.CalledProcessError: try: return _set_env_and_retry(envconfig) except (subprocess.CalledProcessError, NoSuitableVersionFound): if not envconfig.tox_pyenv_fallback: - raise PyenvWhichFailed(err) + raise LOG.debug("tox-pyenv plugin failed, falling back. " "To disable this behavior, set " "tox_pyenv_fallback=False in your tox.ini or use "