@@ -28,6 +28,17 @@ def tox_get_python_executable(envconfig):
28
28
29
29
"""
30
30
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
+
31
42
# __about__
32
43
__title__ = 'tox-pyenv'
33
44
__summary__ = ('tox plugin that makes tox use `pyenv which` '
@@ -41,13 +52,9 @@ def tox_get_python_executable(envconfig):
41
52
# __about__
42
53
43
54
44
- import logging
45
- import subprocess
46
-
47
- import py
48
- from tox import hookimpl as tox_hookimpl
49
-
50
55
LOG = logging .getLogger (__name__ )
56
+ PYTHON_VERSION_RE = re .compile (r'^(?:python|py)([\d\.]{1,8})$' ,
57
+ flags = re .IGNORECASE )
51
58
52
59
53
60
class ToxPyenvException (Exception ):
@@ -65,47 +72,127 @@ class PyenvWhichFailed(ToxPyenvException):
65
72
"""Calling `pyenv which` failed."""
66
73
67
74
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
+
71
79
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 ()]
73
84
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).
77
90
"""
78
91
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
83
95
pipe = subprocess .Popen (
84
96
cmd ,
85
97
stdout = subprocess .PIPE ,
86
98
stderr = subprocess .PIPE ,
87
- universal_newlines = True
99
+ universal_newlines = True ,
100
+ ** popen_kwargs
88
101
)
89
102
out , err = pipe .communicate ()
103
+ out , err = out .strip (), err .strip ()
90
104
except OSError :
91
105
err = '\' pyenv\' : command not found'
92
106
LOG .warning (
93
107
"pyenv doesn't seem to be installed, you probably "
94
108
"don't want this plugin installed either."
95
109
)
96
110
else :
97
- if pipe .poll () == 0 :
111
+ returncode = pipe .poll ()
112
+ if returncode == 0 :
98
113
return out .strip ()
99
114
else :
100
115
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 "
104
123
"tox_pyenv_fallback=False in your tox.ini or use "
105
124
" --tox-pyenv-no-fallback on the command line." ,
106
125
' ' .join ([str (x ) for x in cmd ]), err )
107
126
108
127
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
+
109
196
def _setup_no_fallback (parser ):
110
197
"""Add the option, --tox-pyenv-no-fallback.
111
198
@@ -149,5 +236,5 @@ def _pyenv_fallback(testenv_config, value):
149
236
150
237
@tox_hookimpl
151
238
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."""
153
240
_setup_no_fallback (parser )
0 commit comments