-
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathtox.py
More file actions
263 lines (214 loc) · 8.1 KB
/
tox.py
File metadata and controls
263 lines (214 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
"""
Support for Tox.
Tox is an amazing tool for running tests (and other tasks) in virtualenvs.
You create a ``tox.ini``, tell it what Python versions you want to support
and how to run your test suite, and Tox does everything else: create the
right virtualenvs using the right Python interpreter versions, install your
packages, and run the test commands you specified.
The list of supported Python versions is extracted from ::
[tox]
envlist = py27,py36,py37,py38
"""
import configparser
import re
from typing import Iterable, List, Optional
from .base import Source
from ..parsers.ini import update_ini_setting
from ..utils import FileLines, FileOrFilename, open_file, warn
from ..versions import SortedVersionList, Version, VersionList
TOX_INI = 'tox.ini'
def get_tox_ini_python_versions(
filename: FileOrFilename = TOX_INI,
) -> SortedVersionList:
"""Extract supported Python versions from tox.ini."""
conf = configparser.ConfigParser()
try:
with open_file(filename) as fp:
conf.read_file(fp)
envlist = conf.get('tox', 'envlist')
except configparser.Error:
return []
return sorted({
e for e in map(tox_env_to_py_version, parse_envlist(envlist)) if e
})
def split_envlist(envlist: str) -> Iterable[str]:
"""Split an environment list into items.
Tox allows commas or whitespace as separators.
The trick is that commas inside {...} brace groups do not count.
This function does not expand brace groups.
"""
for part in re.split(r'((?:[{][^}]*[}]|[^,{\s])+)|,|\s+', envlist):
# NB: part can be None
part = (part or '').strip()
if part:
yield part
def parse_envlist(envlist: str) -> List[str]:
"""Parse an environment list.
This function expands brace groups.
"""
envs = []
for part in split_envlist(envlist):
envs += brace_expand(part)
return envs
def brace_expand(s: str) -> List[str]:
"""Expand a braced group.
E.g. brace_expand('a{1,2}{b,c}x') == ['a1bx', 'a1cx', 'a2bx', 'a2cx'].
Note that this function doesn't support nested brace groups. I'm not sure
Tox supports them.
"""
m = re.match('^([^{]*)[{]([^}]*)[}](.*)$', s)
if not m:
return [s]
left = m.group(1)
right = m.group(3)
res = []
for alt in m.group(2).split(','):
res += brace_expand(left + alt.strip() + right)
return res
def tox_env_to_py_version(env: str) -> Optional[Version]:
"""Convert a Tox environment name to a Python version.
E.g. py34 becomes '3.4', pypy3 becomes 'PyPy3'.
Unrecognized environments are left alone.
If the environment name has dashes, only the first part is considered,
e.g. py34-django20 becomes '3.4', and jython-docs becomes 'jython'.
"""
if '-' in env:
# e.g. py34-coverage, pypy-subunit
env = env.partition('-')[0]
if env.startswith('pypy'):
return Version.from_string('PyPy' + env[4:])
elif env.startswith('py') and len(env) >= 4 and env[2:].isdigit():
return Version.from_string(f'{env[2]}.{env[3:]}')
elif env.startswith('py') and '.' in env:
return Version.from_string(f'{env[2:]}', has_dot=True)
else:
return None
def update_tox_ini_python_versions(
filename: FileOrFilename,
new_versions: SortedVersionList,
) -> FileLines:
"""Update supported Python versions in tox.ini.
Does not touch the file but returns a list of lines with new file contents.
"""
with open_file(filename) as fp:
orig_lines = fp.readlines()
fp.seek(0)
conf = configparser.ConfigParser()
try:
conf.read_file(fp)
envlist = conf.get('tox', 'envlist')
except configparser.Error as error:
warn(f"Could not parse {fp.name}: {error}")
return orig_lines
new_envlist = update_tox_envlist(envlist, new_versions)
new_lines = update_ini_setting(
orig_lines, 'tox', 'envlist', new_envlist, filename=fp.name,
)
return new_lines
def update_tox_envlist(envlist: str, new_versions: SortedVersionList) -> str:
"""Update an environment list.
Makes sure all Python versions from ``new_versions`` are in the list.
Removes all Python versions not in ``new_versions``. Leaves other
environments (e.g. flake8, docs) alone.
Tries to preserve formatting and braced groups.
"""
# Find a comma outside brace groups and see what whitespace follows it
# (also note that items can be separated with whitespace without a comma,
# but the only whitespace used this way I've seen in the wild was newlines)
m = re.search(r',\s*|\n', re.sub(r'[{][^}]*[}]', '', envlist.strip()))
if m:
sep = m.group()
else:
sep = ','
trailing_comma = envlist.rstrip().endswith(',')
new_envs = [
toxenv_for_version(ver)
for ver in new_versions
]
if 'py{' in envlist or '{py' in envlist:
# Try to preserve braced groups
parts = []
added_vers = False
for part in split_envlist(envlist):
m = re.match(
r'(py[{](?:\d+|py\d*)(?:,(?:\d+|py\d*))*[}])(?P<rest>.*)',
part
)
if m:
keep = [env for env in brace_expand(m.group(1))
if should_keep(env, new_versions)]
parts.append(
'py{' + ','.join(
env[len('py'):] for env in new_envs + keep
) + '}' + m.group('rest')
)
added_vers = True
continue
m = re.match(
r'([{]py(?:\d+|py\d*)(?:,py(?:\d+|py\d*))*[}])(?P<rest>.*)',
part
)
if m:
keep = [env for env in brace_expand(m.group(1))
if should_keep(env, new_versions)]
parts.append(
'{' + ','.join(new_envs + keep) + '}' + m.group('rest')
)
added_vers = True
continue
vers = brace_expand(part)
if all(not should_keep(ver, new_versions) for ver in vers):
continue
if not all(should_keep(ver, new_versions) for ver in vers):
parts.append(sep.join(
ver for ver in vers if should_keep(ver, new_versions)
))
continue
parts.append(part)
if not added_vers:
parts = new_envs + parts
return sep.join(parts)
# Universal expansion, might destroy braced groups
keep_before: List[str] = []
keep_after: List[str] = []
keep = keep_before
for env in parse_envlist(envlist):
if should_keep(env, new_versions):
keep.append(env)
else:
keep = keep_after
new_envlist = sep.join(keep_before + new_envs + keep_after)
if trailing_comma:
new_envlist += ','
return new_envlist
def toxenv_for_version(ver: Version) -> str:
"""Compute a tox environment name for a Python version."""
_ret_str = f"py{ver.major}" \
f"{'.' if ver.has_dot else ''}" \
f"{ver.minor if ver.minor >= 0 else ''}"
return _ret_str
def should_keep(env: str, new_versions: VersionList) -> bool:
"""Check if a tox environment needs to be kept.
Any environments that refer to a specific Python version not in
``new_versions`` will be removed. All other environments are kept.
``pypy`` and ``pypy3`` are kept only if there's at least one Python 2.x
or 3.x version respectively in ``new_versions``.
"""
if not re.match(r'py(py)?(\d[.])?\d*($|-)', env):
return True
if env == 'pypy':
return any(ver.major == 2 for ver in new_versions)
if env == 'pypy3':
return any(ver.major == 3 for ver in new_versions)
if '-' in env:
baseversion = tox_env_to_py_version(env)
if baseversion in new_versions:
return True
return False
Tox = Source(
filename=TOX_INI,
extract=get_tox_ini_python_versions,
update=update_tox_ini_python_versions,
check_pypy_consistency=True,
has_upper_bound=True,
)