From 5947aa2b4067fb9c16e0f442670c4f1db083e9eb Mon Sep 17 00:00:00 2001 From: Bernat Gabor Date: Mon, 11 Mar 2019 18:57:17 +0100 Subject: [PATCH 1/2] implement tox environment provisioning --- docs/changelog/998.feature.rst | 6 + docs/config.rst | 43 ++++--- src/tox/action.py | 1 - src/tox/config/__init__.py | 108 +++++++++--------- src/tox/exception.py | 11 +- src/tox/session/__init__.py | 44 +++---- src/tox/session/commands/provision.py | 31 +++++ src/tox/session/commands/show_env.py | 2 +- tests/integration/test_provision_int.py | 63 ++++++++++ tests/unit/config/test_config.py | 26 ----- .../builder/test_package_builder_isolated.py | 2 +- tests/unit/session/test_provision.py | 82 +++++++++++++ tests/unit/session/test_session.py | 19 --- 13 files changed, 293 insertions(+), 145 deletions(-) create mode 100644 docs/changelog/998.feature.rst create mode 100644 src/tox/session/commands/provision.py create mode 100644 tests/integration/test_provision_int.py create mode 100644 tests/unit/session/test_provision.py diff --git a/docs/changelog/998.feature.rst b/docs/changelog/998.feature.rst new file mode 100644 index 000000000..3f2d5f5ea --- /dev/null +++ b/docs/changelog/998.feature.rst @@ -0,0 +1,6 @@ +Auto provision host tox requirements. In case the host tox does not satisfy either the +:conf:`minversion` or the :conf:`requires`, tox will now automatically create a virtual environment +under :conf:`provision_tox_env` that satisfies those constraints and delegate all calls to this +meta environment. This should allow automatically satisfying constraints on your tox environment, +given you have at least version ``3.8.0`` of tox. Plugins no longer need to be manually satisfied +by the users, increasing their ease of use. diff --git a/docs/config.rst b/docs/config.rst index f3aa8c912..005e2cc8f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -37,7 +37,31 @@ Global settings are defined under the ``tox`` section as: .. conf:: minversion Define the minimal tox version required to run; if the host tox is less than this - the tool with exit with an error message indicating the user needs to upgrade tox. + the tool with create an environment and provision it with a tox that satisfies it + under :conf:`provision_tox_env`. + +.. conf:: requires ^ LIST of PEP-508 + + .. versionadded:: 3.2.0 + + Specify python packages that need to exist alongside the tox installation for the tox build + to be able to start. Use this to specify plugin requirements (or the version of ``virtualenv`` - + determines the default ``pip``, ``setuptools``, and ``wheel`` versions the tox environments + start with). If these dependencies are not specified tox will create :conf:`provision_tox_env` + environment so that they are satisfied and delegate all calls to that. + + .. code-block:: ini + + [tox] + requires = tox-venv + setuptools >= 30.0.0 + +.. conf:: provision_tox_env ^ string ^ .tox + + .. versionadded:: 3.8.0 + + Name of the virtual environment used to provision a tox having all dependencies specified + inside :conf:`requires` and :conf:`minversion`. .. conf:: toxworkdir ^ PATH ^ {toxinidir}/.tox @@ -122,23 +146,6 @@ Global settings are defined under the ``tox`` section as: configure :conf:`basepython` in the global testenv without affecting environments that have implied base python versions. -.. conf:: requires ^ LIST of PEP-508 - - .. versionadded:: 3.2.0 - - Specify python packages that need to exist alongside the tox installation for the tox build - to be able to start. Use this to specify plugin requirements and build dependencies. - - .. code-block:: ini - - [tox] - requires = tox-venv - setuptools >= 30.0.0 - - .. note:: tox does **not** install those required packages for you. tox only checks if the - requirements are satisfied and crashes early with an helpful error rather then later - in the process. - .. conf:: isolated_build ^ true|false ^ false .. versionadded:: 3.3.0 diff --git a/src/tox/action.py b/src/tox/action.py index 20efd8e5a..801aa5261 100644 --- a/src/tox/action.py +++ b/src/tox/action.py @@ -140,7 +140,6 @@ def feed_stdin(self, fin, process, redirect): else: out, err = process.communicate() except KeyboardInterrupt: - reporter.error("KEYBOARDINTERRUPT") process.wait() raise return out diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py index 89aab192a..16d513371 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -21,6 +21,7 @@ import tox from tox.constants import INFO from tox.interpreters import Interpreters, NoInterpreterInfo +from tox.reporter import update_default_reporter from .parallel import ENV_VAR_KEY as PARALLEL_ENV_VAR_KEY from .parallel import add_parallel_config, add_parallel_flags @@ -227,6 +228,7 @@ def parseconfig(args, plugins=()): """ pm = get_plugin_manager(plugins) config, option = parse_cli(args, pm) + update_default_reporter(config.option.quiet_level, config.option.verbose_level) for config_file in propose_configs(option.configfile): config_type = config_file.basename @@ -572,7 +574,7 @@ def basepython_default(testenv_config, value): parser.add_testenv_attribute( name="basepython", - type="string", + type="basepython", default=None, postprocess=basepython_default, help="executable name or path of interpreter used to create a virtual test environment.", @@ -977,25 +979,6 @@ def __init__(self, config, ini_path, ini_data): # noqa reader.addsubstitutions(toxinidir=config.toxinidir, homedir=config.homedir) - # As older versions of tox may have bugs or incompatibilities that - # prevent parsing of tox.ini this must be the first thing checked. - config.minversion = reader.getstring("minversion", None) - if config.minversion: - # As older versions of tox may have bugs or incompatibilities that - # prevent parsing of tox.ini this must be the first thing checked. - config.minversion = reader.getstring("minversion", None) - if config.minversion: - tox_version = pkg_resources.parse_version(tox.__version__) - config_min_version = pkg_resources.parse_version(self.config.minversion) - if config_min_version > tox_version: - raise tox.exception.MinVersionError( - "tox version is {}, required is at least {}".format( - tox.__version__, self.config.minversion - ) - ) - - self.ensure_requires_satisfied(reader.getlist("requires")) - if config.option.workdir is None: config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox") else: @@ -1004,12 +987,19 @@ def __init__(self, config, ini_path, ini_data): # noqa if os.path.exists(str(config.toxworkdir)): config.toxworkdir = config.toxworkdir.realpath() - if config.option.skip_missing_interpreters == "config": - val = reader.getbool("skip_missing_interpreters", False) - config.option.skip_missing_interpreters = "true" if val else "false" - + reader.addsubstitutions(toxworkdir=config.toxworkdir) config.ignore_basepython_conflict = reader.getbool("ignore_basepython_conflict", False) + config.distdir = reader.getpath("distdir", "{toxworkdir}/dist") + + reader.addsubstitutions(distdir=config.distdir) + config.distshare = reader.getpath("distshare", dist_share_default) + config.temp_dir = reader.getpath("temp_dir", "{toxworkdir}/.tmp") + reader.addsubstitutions(distshare=config.distshare) + config.sdistsrc = reader.getpath("sdistsrc", None) + config.setupdir = reader.getpath("setupdir", "{toxinidir}") + config.logdir = config.toxworkdir.join("log") + # determine indexserver dictionary config.indexserver = {"default": IndexServerConfig("default")} prefix = "indexserver" @@ -1017,6 +1007,10 @@ def __init__(self, config, ini_path, ini_data): # noqa name, url = map(lambda x: x.strip(), line.split("=", 1)) config.indexserver[name] = IndexServerConfig(name, url) + if config.option.skip_missing_interpreters == "config": + val = reader.getbool("skip_missing_interpreters", False) + config.option.skip_missing_interpreters = "true" if val else "false" + override = False if config.option.indexurl: for url_def in config.option.indexurl: @@ -1037,16 +1031,7 @@ def __init__(self, config, ini_path, ini_data): # noqa for name in config.indexserver: config.indexserver[name] = IndexServerConfig(name, override) - reader.addsubstitutions(toxworkdir=config.toxworkdir) - config.distdir = reader.getpath("distdir", "{toxworkdir}/dist") - - reader.addsubstitutions(distdir=config.distdir) - config.distshare = reader.getpath("distshare", dist_share_default) - config.temp_dir = reader.getpath("temp_dir", "{toxworkdir}/.tmp") - reader.addsubstitutions(distshare=config.distshare) - config.sdistsrc = reader.getpath("sdistsrc", None) - config.setupdir = reader.getpath("setupdir", "{toxinidir}") - config.logdir = config.toxworkdir.join("log") + self.handle_provision(config, reader) self.parse_build_isolation(config, reader) config.envlist, all_envs = self._getenvdata(reader, config) @@ -1080,32 +1065,43 @@ def __init__(self, config, ini_path, ini_data): # noqa config.skipsdist = reader.getbool("skipsdist", all_develop) - def parse_build_isolation(self, config, reader): - config.isolated_build = reader.getbool("isolated_build", False) - config.isolated_build_env = reader.getstring("isolated_build_env", ".package") - if config.isolated_build is True: - name = config.isolated_build_env - if name not in config.envconfigs: - config.envconfigs[name] = self.make_envconfig( - name, "{}{}".format(testenvprefix, name), reader._subs, config - ) + def handle_provision(self, config, reader): + requires_list = reader.getlist("requires") + config.minversion = reader.getstring("minversion", None) + requires_list.append("tox >= {}".format(config.minversion or tox.__version__)) + config.provision_tox_env = name = reader.getstring("provision_tox_env", ".tox") + env_config = self.make_envconfig( + name, "{}{}".format(testenvprefix, name), reader._subs, config + ) + env_config.deps = [DepConfig(r, None) for r in requires_list] + self.ensure_requires_satisfied(config, env_config) @staticmethod - def ensure_requires_satisfied(specified): + def ensure_requires_satisfied(config, env_config): missing_requirements = [] - for s in specified: + deps = env_config.deps + for require in deps: + # noinspection PyBroadException try: - pkg_resources.get_distribution(s) + pkg_resources.get_distribution(require.name) except pkg_resources.RequirementParseError: raise except Exception: - missing_requirements.append(str(pkg_resources.Requirement(s))) + missing_requirements.append(str(pkg_resources.Requirement(require.name))) + config.run_provision = bool(missing_requirements) if missing_requirements: - raise tox.exception.MissingRequirement( - "Packages {} need to be installed alongside tox in {}".format( - ", ".join(missing_requirements), sys.executable + config.envconfigs[config.provision_tox_env] = env_config + raise tox.exception.MissingRequirement(config) + + def parse_build_isolation(self, config, reader): + config.isolated_build = reader.getbool("isolated_build", False) + config.isolated_build_env = reader.getstring("isolated_build_env", ".package") + if config.isolated_build is True: + name = config.isolated_build_env + if name not in config.envconfigs: + config.envconfigs[name] = self.make_envconfig( + name, "{}{}".format(testenvprefix, name), reader._subs, config ) - ) def _list_section_factors(self, section): factors = set() @@ -1132,6 +1128,11 @@ def make_envconfig(self, name, section, subs, config, replace=True): if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"): meth = getattr(reader, "get{}".format(atype)) res = meth(env_attr.name, env_attr.default, replace=replace) + elif atype == "basepython": + no_fallback = name in (config.provision_tox_env,) + res = reader.getstring( + env_attr.name, env_attr.default, replace=replace, no_fallback=no_fallback + ) elif atype == "space-separated-list": res = reader.getlist(env_attr.name, sep=" ") elif atype == "line-list": @@ -1363,9 +1364,10 @@ def getargvlist(self, name, default="", replace=True): def getargv(self, name, default="", replace=True): return self.getargvlist(name, default, replace=replace)[0] - def getstring(self, name, default=None, replace=True, crossonly=False): + def getstring(self, name, default=None, replace=True, crossonly=False, no_fallback=False): x = None - for s in [self.section_name] + self.fallbacksections: + sections = [self.section_name] + ([] if no_fallback else self.fallbacksections) + for s in sections: try: x = self._cfg[s][name] break diff --git a/src/tox/exception.py b/src/tox/exception.py index 286929d23..e48cfbb66 100644 --- a/src/tox/exception.py +++ b/src/tox/exception.py @@ -1,4 +1,5 @@ import os +import pipes import signal @@ -80,10 +81,8 @@ class MissingDependency(Error): class MissingRequirement(Error): """A requirement defined in :config:`require` is not met.""" + def __init__(self, config): + self.config = config -class MinVersionError(Error): - """The installed tox version is lower than requested minversion.""" - - def __init__(self, message): - self.message = message - super(MinVersionError, self).__init__(message) + def __str__(self): + return " ".join(pipes.quote(i) for i in self.config.requires) diff --git a/src/tox/session/__init__.py b/src/tox/session/__init__.py index 3b36f1c9a..251d74ca3 100644 --- a/src/tox/session/__init__.py +++ b/src/tox/session/__init__.py @@ -29,6 +29,7 @@ from .commands.help import show_help from .commands.help_ini import show_help_ini +from .commands.provision import provision_tox from .commands.run.parallel import run_parallel from .commands.run.sequential import run_sequential from .commands.show_config import show_config @@ -55,7 +56,6 @@ def main(args): setup_reporter(args) try: config = load_config(args) - update_default_reporter(config.option.quiet_level, config.option.verbose_level) reporter.using("tox.ini: {}".format(config.toxinipath)) config.logdir.ensure(dir=1) ensure_empty_dir(config.logdir) @@ -66,19 +66,19 @@ def main(args): raise SystemExit(retcode) except KeyboardInterrupt: raise SystemExit(2) - except (tox.exception.MinVersionError, tox.exception.MissingRequirement) as exception: - reporter.error(str(exception)) - raise SystemExit(1) def load_config(args): - config = parseconfig(args) - if config.option.help: - show_help(config) - raise SystemExit(0) - elif config.option.helpini: - show_help_ini(config) - raise SystemExit(0) + try: + config = parseconfig(args) + if config.option.help: + show_help(config) + raise SystemExit(0) + elif config.option.helpini: + show_help_ini(config) + raise SystemExit(0) + except tox.exception.MissingRequirement as exception: + config = exception.config return config @@ -97,7 +97,7 @@ def _reset(self, config, popen=subprocess.Popen): self.popen = popen self.resultlog = ResultLog() self.existing_venvs = OrderedDict() - self.venv_dict = self._build_venvs() + self.venv_dict = {} if self.config.run_provision else self._build_venvs() def _build_venvs(self): try: @@ -168,15 +168,19 @@ def newaction(self, name, msg, *args): def runcommand(self): reporter.using("tox-{} from {}".format(tox.__version__, tox.__file__)) show_description = reporter.has_level(reporter.Verbosity.DEFAULT) - if self.config.option.showconfig: - self.showconfig() - elif self.config.option.listenvs: - self.showenvs(all_envs=False, description=show_description) - elif self.config.option.listenvs_all: - self.showenvs(all_envs=True, description=show_description) + if self.config.run_provision: + provision_tox_venv = self.getvenv(self.config.provision_tox_env) + provision_tox(provision_tox_venv, self.config.args) else: - with self.cleanup(): - return self.subcommand_test() + if self.config.option.showconfig: + self.showconfig() + elif self.config.option.listenvs: + self.showenvs(all_envs=False, description=show_description) + elif self.config.option.listenvs_all: + self.showenvs(all_envs=True, description=show_description) + else: + with self.cleanup(): + return self.subcommand_test() @contextmanager def cleanup(self): diff --git a/src/tox/session/commands/provision.py b/src/tox/session/commands/provision.py new file mode 100644 index 000000000..f6779efe2 --- /dev/null +++ b/src/tox/session/commands/provision.py @@ -0,0 +1,31 @@ +"""In case the tox environment is not correctly setup provision it and delegate execution""" +import signal +import subprocess + + +def provision_tox(provision_venv, args): + ensure_meta_env_up_to_date(provision_venv) + process = start_meta_tox(args, provision_venv) + result_out = wait_for_meta_tox(process) + raise SystemExit(result_out) + + +def ensure_meta_env_up_to_date(provision_venv): + if provision_venv.setupenv(): + provision_venv.finishvenv() + + +def start_meta_tox(args, provision_venv): + provision_args = [str(provision_venv.envconfig.envpython), "-m", "tox"] + args + process = subprocess.Popen(provision_args) + return process + + +def wait_for_meta_tox(process): + try: + result_out = process.wait() + except KeyboardInterrupt: + # if we try to interrupt delegate interrupt to meta tox + process.send_signal(signal.SIGINT) + result_out = process.wait() + return result_out diff --git a/src/tox/session/commands/show_env.py b/src/tox/session/commands/show_env.py index 02d4d1c61..f741234a0 100644 --- a/src/tox/session/commands/show_env.py +++ b/src/tox/session/commands/show_env.py @@ -6,7 +6,7 @@ def show_envs(config, all_envs=False, description=False): env_conf = config.envconfigs # this contains all environments default = config.envlist # this only the defaults - ignore = {config.isolated_build_env}.union(default) + ignore = {config.isolated_build_env, config.provision_tox_env}.union(default) extra = [e for e in env_conf if e not in ignore] if all_envs else [] if description: diff --git a/tests/integration/test_provision_int.py b/tests/integration/test_provision_int.py new file mode 100644 index 000000000..6cb3b93c1 --- /dev/null +++ b/tests/integration/test_provision_int.py @@ -0,0 +1,63 @@ +import signal +import subprocess +import sys +import time + +import pytest +from pathlib2 import Path + + +def test_provision_missing(initproj, cmd): + initproj( + "pkg123-0.7", + filedefs={ + "tox.ini": """ + [tox] + skipsdist=True + minversion = 3.7.0 + requires = setuptools == 40.6.3 + [testenv] + commands=python -c "import sys; print(sys.executable); raise SystemExit(1)" + """ + }, + ) + result = cmd("-q", "-q") + assert result.ret == 1 + meta_python = Path(result.out.strip()) + assert meta_python.exists() + + +@pytest.mark.skipif( + sys.platform == "win32", reason="no easy way to trigger CTRL+C on windows for a process" +) +def test_provision_interrupt_child(initproj, cmd): + initproj( + "pkg123-0.7", + filedefs={ + "tox.ini": """ + [tox] + skipsdist=True + minversion = 3.7.0 + requires = setuptools == 40.6.3 + [testenv] + commands=python -c "file_h = open('a', 'w').write('b'); \ + import time; time.sleep(10)" + [testenv:b] + basepython=python + """ + }, + ) + cmd = [sys.executable, "-m", "tox", "-v", "-v", "-e", "python"] + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True + ) + signal_file = Path() / "a" + while not signal_file.exists() and process.poll() is None: + time.sleep(0.1) + if process.poll() is not None: + out, err = process.communicate() + assert False, out + + process.send_signal(signal.SIGINT) + out, _ = process.communicate() + assert "\nERROR: keyboardinterrupt\n" in out, out diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 02aff7988..0fad2df3a 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -2217,14 +2217,6 @@ def test_envlist_multiline(self, newconfig): config = newconfig([], inisource) assert config.envlist == ["py27", "py34"] - def test_minversion(self, newconfig): - inisource = """ - [tox] - minversion = 10.0 - """ - with pytest.raises(tox.exception.MinVersionError): - newconfig([], inisource) - def test_skip_missing_interpreters_true(self, newconfig): ini_source = """ [tox] @@ -2921,24 +2913,6 @@ def test_commands_with_backslash(self, newconfig): assert envconfig.commands[0] == ["some", r"hello\world"] -def test_plugin_require(newconfig): - inisource = """ - [tox] - requires = setuptools - name[foo,bar]>=2,<3; python_version>"2.0" and os_name=='a' - b - """ - with pytest.raises(tox.exception.MissingRequirement) as exc_info: - newconfig([], inisource) - - expected = ( - 'Packages name[bar,foo]<3,>=2; python_version > "2.0" and os_name == "a", b ' - "need to be installed alongside tox in {}".format(sys.executable) - ) - actual = exc_info.value.args[0] - assert actual == expected - - def test_isolated_build_env_cannot_be_in_envlist(newconfig, capsys): inisource = """ [tox] diff --git a/tests/unit/package/builder/test_package_builder_isolated.py b/tests/unit/package/builder/test_package_builder_isolated.py index 38e1579ee..4e062c178 100644 --- a/tests/unit/package/builder/test_package_builder_isolated.py +++ b/tests/unit/package/builder/test_package_builder_isolated.py @@ -80,7 +80,7 @@ def toml_file_check(initproj, version, message, toml): }, ) - with pytest.raises(SystemExit, message=1): + with pytest.raises(SystemExit, match="1"): get_build_info(py.path.local()) toml_file = py.path.local().join("pyproject.toml") msg = "ERROR: {} inside {}".format(message, toml_file) diff --git a/tests/unit/session/test_provision.py b/tests/unit/session/test_provision.py new file mode 100644 index 000000000..50e6f0dc9 --- /dev/null +++ b/tests/unit/session/test_provision.py @@ -0,0 +1,82 @@ +import sys + +import pytest + +from tox.exception import MissingRequirement + + +@pytest.fixture(scope="session") +def next_tox_major(): + """a tox version we can guarantee to not be available""" + return "10.0.0" + + +def test_provision_min_version_is_requires(newconfig, next_tox_major): + with pytest.raises(MissingRequirement) as context: + newconfig( + [], + """ + [tox] + minversion = {} + """.format( + next_tox_major + ), + ) + config = context.value.config + + deps = [r.name for r in config.envconfigs[config.provision_tox_env].deps] + assert deps == ["tox >= {}".format(next_tox_major)] + assert config.run_provision is True + assert config.toxworkdir + assert config.toxinipath + assert config.provision_tox_env == ".tox" + assert config.ignore_basepython_conflict is False + + +def test_provision_tox_change_name(newconfig): + config = newconfig( + [], + """ + [tox] + provision_tox_env = magic + """, + ) + assert config.provision_tox_env == "magic" + + +def test_provision_basepython_global_only(newconfig, next_tox_major): + """we don't want to inherit basepython from global""" + with pytest.raises(MissingRequirement) as context: + newconfig( + [], + """ + [tox] + minversion = {} + [testenv] + basepython = what + """.format( + next_tox_major + ), + ) + config = context.value.config + base_python = config.envconfigs[".tox"].basepython + assert base_python == sys.executable + + +def test_provision_basepython_local(newconfig, next_tox_major): + """however adhere to basepython when explicilty set""" + with pytest.raises(MissingRequirement) as context: + newconfig( + [], + """ + [tox] + minversion = {} + [testenv:.tox] + basepython = what + """.format( + next_tox_major + ), + ) + config = context.value.config + base_python = config.envconfigs[".tox"].basepython + assert base_python == "what" diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index ae7592823..745de8de7 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -1,5 +1,4 @@ import os -import re import sys import textwrap from threading import Thread @@ -68,24 +67,6 @@ def test_resolve_pkg_doubledash(tmpdir, mocksession): assert res == p -def test_minversion(cmd, initproj): - initproj( - "interp123-0.5", - filedefs={ - "tests": {"test_hello.py": "def test_hello(): pass"}, - "tox.ini": """ - [tox] - minversion = 6.0 - """, - }, - ) - result = cmd("-v") - assert re.match( - r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out - ) - assert result.ret - - def test_skip_sdist(cmd, initproj): initproj( "pkg123-0.7", From cf65fb9b83b9867aca3e8af7bca00693db08437a Mon Sep 17 00:00:00 2001 From: Bernat Gabor Date: Mon, 11 Mar 2019 23:33:46 +0100 Subject: [PATCH 2/2] improve documentation on auto-provision --- docs/changelog/998.feature.rst | 8 ++------ docs/config.rst | 2 ++ docs/example/basic.rst | 26 ++++++++++++++++++++++++++ tests/unit/test_docs.py | 2 +- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/changelog/998.feature.rst b/docs/changelog/998.feature.rst index 3f2d5f5ea..089ac8d77 100644 --- a/docs/changelog/998.feature.rst +++ b/docs/changelog/998.feature.rst @@ -1,6 +1,2 @@ -Auto provision host tox requirements. In case the host tox does not satisfy either the -:conf:`minversion` or the :conf:`requires`, tox will now automatically create a virtual environment -under :conf:`provision_tox_env` that satisfies those constraints and delegate all calls to this -meta environment. This should allow automatically satisfying constraints on your tox environment, -given you have at least version ``3.8.0`` of tox. Plugins no longer need to be manually satisfied -by the users, increasing their ease of use. +tox now auto-provisions itself if needed (see :ref:`auto-provision`). Plugins or minimum version of tox no longer +need to be manually satisfied by the user, increasing their ease of use. diff --git a/docs/config.rst b/docs/config.rst index 005e2cc8f..43098b102 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -225,6 +225,7 @@ Complete list of settings that you can put into ``testenv*`` sections: Use this to specify the python version for a tox environment. If not specified, the virtual environments factors (e.g. name part) will be used to automatically set one. For example, ``py37`` means ``python3.7``, ``py3`` means ``python3`` and ``py`` means ``python``. + :conf:`provision_tox_env` environment does not inherit this setting from the ``toxenv`` section. .. versionchanged:: 3.1 @@ -233,6 +234,7 @@ Complete list of settings that you can put into ``testenv*`` sections: :conf:`ignore_basepython_conflict` is set, the value is ignored and we force the ``basepython`` implied from the factor name. + .. conf:: commands ^ ARGVLIST The commands to be called for testing. Only execute if :conf:`commands_pre` succeed. diff --git a/docs/example/basic.rst b/docs/example/basic.rst index d76356199..b3d4c1728 100644 --- a/docs/example/basic.rst +++ b/docs/example/basic.rst @@ -437,3 +437,29 @@ Example progress bar, showing a rotating spinner, the number of environments run .. code-block:: bash ⠹ [2] py27 | py36 + +.. _`auto-provision`: + +tox auto-provisioning +--------------------- +In case the host tox does not satisfy either the :conf:`minversion` or the :conf:`requires`, tox will now automatically +create a virtual environment under :conf:`provision_tox_env` that satisfies those constraints and delegate all calls +to this meta environment. This should allow automatically satisfying constraints on your tox environment, +given you have at least version ``3.8.0`` of tox. + +For example given: + +.. code-block:: ini + + [tox] + minversion = 3.10.0 + requires = tox_venv >= 1.0.0 + +if the user runs it with tox ``3.8.0`` or later installed tox will automatically ensured that both the minimum version +and requires constraints are satisfied, by creating a virtual environment under ``.tox`` folder, and then installing +into it ``tox >= 3.10.0`` and ``tox_venv >= 1.0.0``. Afterwards all tox invocations are forwarded to the tox installed +inside ``.tox\.tox`` folder (referred to as meta-tox or auto-provisioned tox). + +This allows tox to automatically setup itself with all its plugins for the current project. If the host tox satisfies +the constraints expressed with the :conf:`requires` and :conf:`minversion` no such provisioning is done (to avoid +setup cost when it's not explicitly needed). diff --git a/tests/unit/test_docs.py b/tests/unit/test_docs.py index d62fa7846..380bb61ce 100644 --- a/tests/unit/test_docs.py +++ b/tests/unit/test_docs.py @@ -41,7 +41,7 @@ def test_all_rst_ini_blocks_parse(filename, tmpdir): try: parseconfig(["-c", str(config_path)]) except tox.exception.MissingRequirement: - assert "requires = tox-venv" in str(code) + pass except Exception as e: raise AssertionError( "Error parsing ini block\n\n"