Skip to content

Commit 93a9667

Browse files
authored
Add --no-provision flag (#1922)
tox can now be invoked with a new --no-provision flag that prevents provision, if requires or minversion are not satisfied, tox will fail; if a path is specified as an argument to the flag (e.g. as `tox --no-provision missing.json`) and provision is prevented, provision metadata are written as JSON to that path. Fixes #1921
1 parent a586b2a commit 93a9667

File tree

4 files changed

+149
-2
lines changed

4 files changed

+149
-2
lines changed

docs/changelog/1921.feature.rst

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
tox can now be invoked with a new ``--no-provision`` flag that prevents provision,
2+
if :conf:`requires` or :conf:`minversion` are not satisfied,
3+
tox will fail;
4+
if a path is specified as an argument to the flag
5+
(e.g. as ``tox --no-provision missing.json``) and provision is prevented,
6+
provision metadata are written as JSON to that path - by :user:`hroncok`

docs/config.rst

+15
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ Global settings are defined under the ``tox`` section as:
3838
than this the tool will create an environment and provision it with a version of
3939
tox that satisfies this under :conf:`provision_tox_env`.
4040

41+
.. versionchanged:: 3.23.0
42+
43+
When tox is invoked with the ``--no-provision`` flag,
44+
the provision won't be attempted, tox will fail instead.
45+
4146
.. conf:: requires ^ LIST of PEP-508
4247

4348
.. versionadded:: 3.2.0
@@ -54,13 +59,23 @@ Global settings are defined under the ``tox`` section as:
5459
requires = tox-pipenv
5560
setuptools >= 30.0.0
5661
62+
.. versionchanged:: 3.23.0
63+
64+
When tox is invoked with the ``--no-provision`` flag,
65+
the provision won't be attempted, tox will fail instead.
66+
5767
.. conf:: provision_tox_env ^ string ^ .tox
5868

5969
.. versionadded:: 3.8.0
6070

6171
Name of the virtual environment used to provision a tox having all dependencies specified
6272
inside :conf:`requires` and :conf:`minversion`.
6373

74+
.. versionchanged:: 3.23.0
75+
76+
When tox is invoked with the ``--no-provision`` flag,
77+
the provision won't be attempted, tox will fail instead.
78+
6479
.. conf:: toxworkdir ^ PATH ^ {toxinidir}/.tox
6580

6681
Directory for tox to generate its environments into, will be created if it does not exist.

src/tox/config/__init__.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import itertools
5+
import json
56
import os
67
import random
78
import re
@@ -572,6 +573,16 @@ def tox_addoption(parser):
572573
action="store_true",
573574
help="override alwayscopy setting to True in all envs",
574575
)
576+
parser.add_argument(
577+
"--no-provision",
578+
action="store",
579+
nargs="?",
580+
default=False,
581+
const=True,
582+
metavar="REQUIRES_JSON",
583+
help="do not perform provision, but fail and if a path was provided "
584+
"write provision metadata as JSON to it",
585+
)
575586

576587
cli_skip_missing_interpreter(parser)
577588
parser.add_argument("--workdir", metavar="PATH", help="tox working directory")
@@ -1318,8 +1329,8 @@ def handle_provision(self, config, reader):
13181329
# raise on unknown args
13191330
self.config._parser.parse_cli(args=self.config.args, strict=True)
13201331

1321-
@staticmethod
1322-
def ensure_requires_satisfied(config, requires, min_version):
1332+
@classmethod
1333+
def ensure_requires_satisfied(cls, config, requires, min_version):
13231334
missing_requirements = []
13241335
failed_to_parse = False
13251336
deps = []
@@ -1346,12 +1357,33 @@ def ensure_requires_satisfied(config, requires, min_version):
13461357
missing_requirements.append(str(requirements.Requirement(require)))
13471358
if failed_to_parse:
13481359
raise tox.exception.BadRequirement()
1360+
if config.option.no_provision and missing_requirements:
1361+
msg = "provisioning explicitly disabled within {}, but missing {}"
1362+
if config.option.no_provision is not True: # it's a path
1363+
msg += " and wrote to {}"
1364+
cls.write_requires_to_json_file(config)
1365+
raise tox.exception.Error(
1366+
msg.format(sys.executable, missing_requirements, config.option.no_provision)
1367+
)
13491368
if WITHIN_PROVISION and missing_requirements:
13501369
msg = "break infinite loop provisioning within {} missing {}"
13511370
raise tox.exception.Error(msg.format(sys.executable, missing_requirements))
13521371
config.run_provision = bool(len(missing_requirements))
13531372
return deps
13541373

1374+
@staticmethod
1375+
def write_requires_to_json_file(config):
1376+
requires_dict = {
1377+
"minversion": config.minversion,
1378+
"requires": config.requires,
1379+
}
1380+
try:
1381+
with open(config.option.no_provision, "w", encoding="utf-8") as outfile:
1382+
json.dump(requires_dict, outfile, indent=4)
1383+
except TypeError: # Python 2
1384+
with open(config.option.no_provision, "w") as outfile:
1385+
json.dump(requires_dict, outfile, indent=4, encoding="utf-8")
1386+
13551387
def parse_build_isolation(self, config, reader):
13561388
config.isolated_build = reader.getbool("isolated_build", False)
13571389
config.isolated_build_env = reader.getstring("isolated_build_env", ".package")

tests/unit/session/test_provision.py

+94
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import, unicode_literals
22

3+
import json
34
import os
45
import shutil
56
import subprocess
@@ -185,6 +186,99 @@ def test_provision_cli_args_not_ignored_if_provision_false(cmd, initproj):
185186
result.assert_fail(is_run_test_env=False)
186187

187188

189+
parametrize_json_path = pytest.mark.parametrize("json_path", [None, "missing.json"])
190+
191+
192+
@parametrize_json_path
193+
def test_provision_does_not_fail_with_no_provision_no_reason(cmd, initproj, json_path):
194+
p = initproj("test-0.1", {"tox.ini": "[tox]"})
195+
result = cmd("--no-provision", *([json_path] if json_path else []))
196+
result.assert_success(is_run_test_env=True)
197+
assert not (p / "missing.json").exists()
198+
199+
200+
@parametrize_json_path
201+
def test_provision_fails_with_no_provision_next_tox(cmd, initproj, next_tox_major, json_path):
202+
p = initproj(
203+
"test-0.1",
204+
{
205+
"tox.ini": """\
206+
[tox]
207+
minversion = {}
208+
""".format(
209+
next_tox_major,
210+
)
211+
},
212+
)
213+
result = cmd("--no-provision", *([json_path] if json_path else []))
214+
result.assert_fail(is_run_test_env=False)
215+
if json_path:
216+
missing = json.loads((p / json_path).read_text("utf-8"))
217+
assert missing["minversion"] == next_tox_major
218+
219+
220+
@parametrize_json_path
221+
def test_provision_fails_with_no_provision_missing_requires(cmd, initproj, json_path):
222+
p = initproj(
223+
"test-0.1",
224+
{
225+
"tox.ini": """\
226+
[tox]
227+
requires =
228+
virtualenv > 99999999
229+
"""
230+
},
231+
)
232+
result = cmd("--no-provision", *([json_path] if json_path else []))
233+
result.assert_fail(is_run_test_env=False)
234+
if json_path:
235+
missing = json.loads((p / json_path).read_text("utf-8"))
236+
assert missing["requires"] == ["virtualenv > 99999999"]
237+
238+
239+
@parametrize_json_path
240+
def test_provision_does_not_fail_with_satisfied_requires(cmd, initproj, next_tox_major, json_path):
241+
p = initproj(
242+
"test-0.1",
243+
{
244+
"tox.ini": """\
245+
[tox]
246+
minversion = 0
247+
requires =
248+
setuptools > 2
249+
pip > 3
250+
"""
251+
},
252+
)
253+
result = cmd("--no-provision", *([json_path] if json_path else []))
254+
result.assert_success(is_run_test_env=True)
255+
assert not (p / "missing.json").exists()
256+
257+
258+
@parametrize_json_path
259+
def test_provision_fails_with_no_provision_combined(cmd, initproj, next_tox_major, json_path):
260+
p = initproj(
261+
"test-0.1",
262+
{
263+
"tox.ini": """\
264+
[tox]
265+
minversion = {}
266+
requires =
267+
setuptools > 2
268+
pip > 3
269+
""".format(
270+
next_tox_major,
271+
)
272+
},
273+
)
274+
result = cmd("--no-provision", *([json_path] if json_path else []))
275+
result.assert_fail(is_run_test_env=False)
276+
if json_path:
277+
missing = json.loads((p / json_path).read_text("utf-8"))
278+
assert missing["minversion"] == next_tox_major
279+
assert missing["requires"] == ["setuptools > 2", "pip > 3"]
280+
281+
188282
@pytest.fixture(scope="session")
189283
def wheel(tmp_path_factory):
190284
"""create a wheel for a project"""

0 commit comments

Comments
 (0)