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

Commit 524795b

Browse files
committed
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
1 parent 7391610 commit 524795b

File tree

1 file changed

+110
-23
lines changed

1 file changed

+110
-23
lines changed

tox_pyenv.py

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ def tox_get_python_executable(envconfig):
2828
2929
"""
3030

31+
import logging
32+
import ntpath
33+
import os
34+
import re
35+
import subprocess
36+
37+
from distutils.version import LooseVersion
38+
39+
import py
40+
from tox import hookimpl as tox_hookimpl
41+
3142
# __about__
3243
__title__ = 'tox-pyenv'
3344
__summary__ = ('tox plugin that makes tox use `pyenv which` '
@@ -41,13 +52,9 @@ def tox_get_python_executable(envconfig):
4152
# __about__
4253

4354

44-
import logging
45-
import subprocess
46-
47-
import py
48-
from tox import hookimpl as tox_hookimpl
49-
5055
LOG = logging.getLogger(__name__)
56+
PYTHON_VERSION_RE = re.compile(r'^(?:python|py)([\d\.]{1,8})$',
57+
flags=re.IGNORECASE)
5158

5259

5360
class ToxPyenvException(Exception):
@@ -65,47 +72,127 @@ class PyenvWhichFailed(ToxPyenvException):
6572
"""Calling `pyenv which` failed."""
6673

6774

68-
@tox_hookimpl
69-
def tox_get_python_executable(envconfig):
70-
"""Return a python executable for the given python base name.
75+
class NoSuitableVersionFound(ToxPyenvException):
76+
77+
"""Could not a find a python version that satisfies requirement."""
78+
7179

72-
The first plugin/hook which returns an executable path will determine it.
80+
def _get_pyenv_known_versions():
81+
"""Return searchable output from `pyenv versions`."""
82+
known_versions = _pyenv_run(['versions'])[0].split(os.linesep)
83+
return [v.strip() for v in known_versions if v.strip()]
7384

74-
``envconfig`` is the testenv configuration which contains
75-
per-testenv configuration, notably the ``.envname`` and ``.basepython``
76-
setting.
85+
86+
def _pyenv_run(command, **popen_kwargs):
87+
"""Run pyenv command with Popen.
88+
89+
Returns the result tuple as (stdout, stderr, returncode).
7790
"""
7891
try:
79-
# pylint: disable=no-member
80-
pyenv = (getattr(py.path.local.sysfind('pyenv'), 'strpath', 'pyenv')
81-
or 'pyenv')
82-
cmd = [pyenv, 'which', envconfig.basepython]
92+
pyenv = (getattr(
93+
py.path.local.sysfind('pyenv'), 'strpath', 'pyenv') or 'pyenv')
94+
cmd = [pyenv] + command
8395
pipe = subprocess.Popen(
8496
cmd,
8597
stdout=subprocess.PIPE,
8698
stderr=subprocess.PIPE,
87-
universal_newlines=True
99+
universal_newlines=True,
100+
**popen_kwargs
88101
)
89102
out, err = pipe.communicate()
103+
out, err = out.strip(), err.strip()
90104
except OSError:
91105
err = '\'pyenv\': command not found'
92106
LOG.warning(
93107
"pyenv doesn't seem to be installed, you probably "
94108
"don't want this plugin installed either."
95109
)
96110
else:
97-
if pipe.poll() == 0:
111+
returncode = pipe.poll()
112+
if returncode == 0:
98113
return out.strip()
99114
else:
100115
if not envconfig.tox_pyenv_fallback:
101-
raise PyenvWhichFailed(err)
102-
LOG.debug("`%s` failed thru tox-pyenv plugin, falling back. "
103-
"STDERR: \"%s\" | To disable this behavior, set "
116+
cmdstr = ' '.join([str(x) for x in cmd])
117+
LOG.error("The command `%s` executed by the tox-pyenv plugin failed. "
118+
"STDERR: \"%s\" STDOUT: \"%s\"", cmdstr, err, out)
119+
raise subprocess.CalledProcessError(returncode, cmdstr, output=err)
120+
LOG.error("`%s` failed thru tox-pyenv plugin, falling back to "
121+
"tox's built-in behavior. "
122+
"STDERR: \"%s\" | To disable this fallback, set "
104123
"tox_pyenv_fallback=False in your tox.ini or use "
105124
" --tox-pyenv-no-fallback on the command line.",
106125
' '.join([str(x) for x in cmd]), err)
107126

108127

128+
def _extrapolate_to_known_version(desired, known):
129+
"""Given the desired version, find an acceptable available version."""
130+
match = PYTHON_VERSION_RE.match(desired)
131+
if match:
132+
match = match.groups()[0]
133+
if match in known:
134+
return match
135+
else:
136+
matches = sorted([LooseVersion(j) for j in known
137+
if j.startswith(match)])
138+
if matches:
139+
# Select the latest.
140+
# e.g. python2 gets 2.7.10
141+
# if known_versions = ['2.7.3', '2.7', '2.7.10']
142+
return matches[-1].vstring
143+
raise NoSuitableVersionFound(
144+
'Given desired version {0}, no suitable version of python could '
145+
'be matched in the list given by `pyenv versions`.'.format(desired))
146+
147+
148+
def _set_env_and_retry(envconfig):
149+
# Let's be smart, and resilient to 'command not found'
150+
# especially if we can reasonably figure out which
151+
# version of python is desired, and that version of python
152+
# is installed and available through pyenv.
153+
desired_version = ntpath.basename(envconfig.basepython)
154+
LOG.debug("tox-pyenv is now looking for the desired python "
155+
"version (%s) through pyenv. If it is found, it will "
156+
"be enabled and this operation retried.", desired_version)
157+
158+
def _enable_and_call(_available_version):
159+
LOG.debug('Enabling %s by setting $PYENV_VERSION to %s',
160+
desired_version, _available_version)
161+
_env = os.environ.copy()
162+
_env['PYENV_VERSION'] = _available_version
163+
return _pyenv_run(
164+
['which', envconfig.basepython], env=_env)[0]
165+
166+
known_versions = _get_pyenv_known_versions()
167+
168+
if desired_version in known_versions:
169+
return _enable_and_call(desired_version)
170+
else:
171+
match = _extrapolate_to_known_version(
172+
desired_version, known_versions)
173+
return _enable_and_call(match)
174+
175+
176+
@tox_hookimpl
177+
def tox_get_python_executable(envconfig):
178+
"""Hook into tox plugins to use pyenv to find executables."""
179+
180+
try:
181+
out, err = _pyenv_run(['which', envconfig.basepython])
182+
except subprocess.CalledProcessError:
183+
try:
184+
return _set_env_and_retry(envconfig)
185+
except (subprocess.CalledProcessError, NoSuitableVersionFound):
186+
if not envconfig.tox_pyenv_fallback:
187+
raise PyenvWhichFailed(err)
188+
LOG.debug("tox-pyenv plugin failed, falling back. "
189+
"To disable this behavior, set "
190+
"tox_pyenv_fallback=False in your tox.ini or use "
191+
" --tox-pyenv-no-fallback on the command line.")
192+
else:
193+
return out
194+
195+
109196
def _setup_no_fallback(parser):
110197
"""Add the option, --tox-pyenv-no-fallback.
111198
@@ -149,5 +236,5 @@ def _pyenv_fallback(testenv_config, value):
149236

150237
@tox_hookimpl
151238
def tox_addoption(parser):
152-
"""Add command line option to the argparse-style parser object."""
239+
"""Add command line options to the argparse-style parser object."""
153240
_setup_no_fallback(parser)

0 commit comments

Comments
 (0)