@@ -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,5})$' ,
57
+ flags = re .IGNORECASE )
51
58
52
59
53
60
class ToxPyenvException (Exception ):
@@ -65,33 +72,114 @@ class PyenvWhichFailed(ToxPyenvException):
65
72
"""Calling `pyenv which` failed."""
66
73
67
74
68
- @tox_hookimpl
69
- def tox_get_python_executable (envconfig ):
75
+ class NoSuitableVersionFound (ToxPyenvException ):
76
+
77
+ """Could not a find a python version that satisfies requirement."""
78
+
79
+
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 ()]
84
+
85
+
86
+ def _pyenv_run (command , ** popen_kwargs ):
87
+ """Run pyenv command with Popen.
88
+
89
+ Returns the result tuple as (stdout, stderr, returncode).
90
+ """
70
91
try :
71
- pyenv = (getattr (py . path . local . sysfind ( 'pyenv' ), 'strpath' , 'pyenv' )
72
- or 'pyenv' )
73
- cmd = [pyenv , 'which' , envconfig . basepython ]
92
+ pyenv = (getattr (
93
+ py . path . local . sysfind ( 'pyenv' ), 'strpath' , 'pyenv' ) or 'pyenv' )
94
+ cmd = [pyenv ] + command
74
95
pipe = subprocess .Popen (
75
96
cmd ,
76
97
stdout = subprocess .PIPE ,
77
98
stderr = subprocess .PIPE ,
78
- universal_newlines = True
99
+ universal_newlines = True ,
100
+ ** popen_kwargs
79
101
)
80
102
out , err = pipe .communicate ()
103
+ out , err = out .strip (), err .strip ()
81
104
except OSError :
82
105
raise PyenvMissing (
83
106
"pyenv doesn't seem to be installed, you probably "
84
- "don't want this plugin installed either." )
85
- if pipe .poll () == 0 :
86
- return out .strip ()
107
+ "don't want this plugin (tox-pyenv) installed either." )
108
+ returncode = pipe .poll ()
109
+ if returncode != 0 :
110
+ cmdstr = ' ' .join ([str (x ) for x in cmd ])
111
+ LOG .debug ("The command `%s` executed by the tox-pyenv plugin failed. "
112
+ "STDERR: \" %s\" STDOUT: \" %s\" " , cmdstr , err , out )
113
+ raise subprocess .CalledProcessError (returncode , cmdstr , output = err )
114
+ return out , err
115
+
116
+
117
+ def _extrapolate_to_known_version (desired , known ):
118
+ """Given the desired version, find an acceptable available version."""
119
+ match = PYTHON_VERSION_RE .match (desired )
120
+ if match :
121
+ match = match .groups ()[0 ]
122
+ if match in known :
123
+ return match
124
+ else :
125
+ matches = sorted ([LooseVersion (j ) for j in known
126
+ if j .startswith (match )])
127
+ if matches :
128
+ # Select the latest.
129
+ # e.g. python2 gets 2.7.10
130
+ # if known_versions = ['2.7.3', '2.7', '2.7.10']
131
+ return matches [- 1 ].vstring
132
+ raise NoSuitableVersionFound (
133
+ 'Given desired version {0}, no suitable version of python could '
134
+ 'be matched in the list given by `pyenv versions`.' .format (desired ))
135
+
136
+
137
+ def _set_env_and_retry (envconfig ):
138
+ # Let's be smart, and resilient to 'command not found'
139
+ # especially if we can reasonably figure out which
140
+ # version of python is desired, and that version of python
141
+ # is installed and available through pyenv.
142
+ desired_version = ntpath .basename (envconfig .basepython )
143
+ LOG .debug ("tox-pyenv is now looking for the desired python "
144
+ "version (%s) through pyenv. If it is found, it will "
145
+ "be enabled and this operation retried." , desired_version )
146
+
147
+ def _enable_and_call (_available_version ):
148
+ LOG .debug ('Enabling %s by setting $PYENV_VERSION to %s' ,
149
+ desired_version , _available_version )
150
+ _env = os .environ .copy ()
151
+ _env ['PYENV_VERSION' ] = _available_version
152
+ return _pyenv_run (
153
+ ['which' , envconfig .basepython ], env = _env )[0 ]
154
+
155
+ known_versions = _get_pyenv_known_versions ()
156
+
157
+ if desired_version in known_versions :
158
+ return _enable_and_call (desired_version )
159
+ else :
160
+ match = _extrapolate_to_known_version (
161
+ desired_version , known_versions )
162
+ return _enable_and_call (match )
163
+
164
+
165
+ @tox_hookimpl
166
+ def tox_get_python_executable (envconfig ):
167
+ """Hook into tox plugins to use pyenv to find executables."""
168
+
169
+ try :
170
+ out , err = _pyenv_run (['which' , envconfig .basepython ])
171
+ except subprocess .CalledProcessError :
172
+ try :
173
+ return _set_env_and_retry (envconfig )
174
+ except (subprocess .CalledProcessError , NoSuitableVersionFound ):
175
+ if not envconfig .tox_pyenv_fallback :
176
+ raise PyenvWhichFailed (err )
177
+ LOG .debug ("tox-pyenv plugin failed, falling back. "
178
+ "To disable this behavior, set "
179
+ "tox_pyenv_fallback=False in your tox.ini or use "
180
+ " --tox-pyenv-no-fallback on the command line." )
87
181
else :
88
- if not envconfig .tox_pyenv_fallback :
89
- raise PyenvWhichFailed (err )
90
- LOG .debug ("`%s` failed thru tox-pyenv plugin, falling back. "
91
- "STDERR: \" %s\" | To disable this behavior, set "
92
- "tox_pyenv_fallback=False in your tox.ini or use "
93
- " --tox-pyenv-no-fallback on the command line." ,
94
- ' ' .join ([str (x ) for x in cmd ]), err )
182
+ return out
95
183
96
184
97
185
def _setup_no_fallback (parser ):
@@ -137,4 +225,5 @@ def _pyenv_fallback(testenv_config, value):
137
225
138
226
@tox_hookimpl
139
227
def tox_addoption (parser ):
228
+ """Add the --tox-pyenv-no-fallback command line option to tox."""
140
229
_setup_no_fallback (parser )
0 commit comments