diff options
author | Bernát Gábor <gaborjbernat@gmail.com> | 2019-03-12 10:31:40 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-12 10:31:40 +0100 |
commit | 5b6889731555508b4347f9384e556109b7fbe627 (patch) | |
tree | cfd69674ab7c2ab869e35dbc930f8c2a31afa813 /src | |
parent | d21b14de2ad670ad29479cc6fc4f26483c157974 (diff) | |
download | tox-git-5b6889731555508b4347f9384e556109b7fbe627.tar.gz |
implement tox environment provisioning (#1185)
Resolves #998.
Diffstat (limited to 'src')
-rw-r--r-- | src/tox/action.py | 1 | ||||
-rw-r--r-- | src/tox/config/__init__.py | 108 | ||||
-rw-r--r-- | src/tox/exception.py | 11 | ||||
-rw-r--r-- | src/tox/session/__init__.py | 44 | ||||
-rw-r--r-- | src/tox/session/commands/provision.py | 31 | ||||
-rw-r--r-- | src/tox/session/commands/show_env.py | 2 |
6 files changed, 116 insertions, 81 deletions
diff --git a/src/tox/action.py b/src/tox/action.py index 20efd8e5..801aa526 100644 --- a/src/tox/action.py +++ b/src/tox/action.py @@ -140,7 +140,6 @@ class Action(object): 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 89aab192..16d51337 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -21,6 +21,7 @@ import toml 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 tox_addoption(parser): 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 @@ class ParseIni(object): 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 @@ class ParseIni(object): 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 @@ class ParseIni(object): 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 @@ class ParseIni(object): 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 @@ class ParseIni(object): 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 @@ class ParseIni(object): 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 @@ class SectionReader: 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 286929d2..e48cfbb6 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 3b36f1c9..251d74ca 100644 --- a/src/tox/session/__init__.py +++ b/src/tox/session/__init__.py @@ -29,6 +29,7 @@ from tox.venv import VirtualEnv 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 @@ class Session(object): 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 @@ class Session(object): 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 00000000..f6779efe --- /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 02d4d1c6..f741234a 100644 --- a/src/tox/session/commands/show_env.py +++ b/src/tox/session/commands/show_env.py @@ -6,7 +6,7 @@ from tox import reporter as report 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: |