Skip to content
This repository has been archived by the owner on Jul 14, 2023. It is now read-only.

WIP: dont fail if the version is available #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
147 changes: 117 additions & 30 deletions tox_pyenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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` '
Expand All @@ -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):
Expand All @@ -60,50 +67,130 @@ class PyenvMissing(ToxPyenvException, RuntimeError):
"""The pyenv program is not installed."""


class PyenvWhichFailed(ToxPyenvException):
class NoSuitableVersionFound(ToxPyenvException):

"""Calling `pyenv which` failed."""
"""Could not a find a python version that satisfies requirement."""


@tox_hookimpl
def tox_get_python_executable(envconfig):
"""Return a python executable for the given python base name.
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()]

The first plugin/hook which returns an executable path will determine it.

``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_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,
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(
"pyenv doesn't seem to be installed, you probably "
"don't want this plugin installed either."
)
raise
else:
if pipe.poll() == 0:
return out.strip()
returncode = pipe.poll()
if returncode == 0:
return out, err
else:
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):
"""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 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)
LOG.debug("`%s` failed thru tox-pyenv plugin, falling back. "
"STDERR: \"%s\" | To disable this behavior, 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)
raise
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):
Expand Down Expand Up @@ -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)