diff options
| author | Andre Caron <andre.caron@ubisoft.com> | 2017-05-18 13:54:22 -0400 |
|---|---|---|
| committer | Andre Caron <andre.caron@ubisoft.com> | 2017-05-18 15:53:03 -0400 |
| commit | 10b0d6c12ce041b4fe588c194ac9d80d82bd8d98 (patch) | |
| tree | 0b002aa5d18222693014e5793511e37170cdda84 | |
| parent | 2b2a51f8cd845fb6df3f87bb6e83f3e9cc72ab28 (diff) | |
| download | tox-git-10b0d6c12ce041b4fe588c194ac9d80d82bd8d98.tar.gz | |
Isolate environment variable substitutions
Perform `{env:X}` replacement only in environments where `X` is used. This
enables different environments to use different subsets of the environment
(e.g. don't require union of all passenv sections in all environments).
| -rw-r--r-- | CHANGELOG | 2 | ||||
| -rw-r--r-- | tests/test_config.py | 20 | ||||
| -rwxr-xr-x | tox/config.py | 68 |
3 files changed, 58 insertions, 32 deletions
@@ -2,6 +2,8 @@ Not released yet ---- - #474: Start using setuptools_scm for tag based versioning. +- #515: Don't require environment variables in test environments + where they are not used. 2.7.0 ----- diff --git a/tests/test_config.py b/tests/test_config.py index d03a5d29..5ef05642 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1075,6 +1075,26 @@ class TestConfigTestEnv: assert 'FOO' in env assert 'BAR' in env + def test_substitution_notfound_issue515(tmpdir, newconfig): + config = newconfig(""" + [tox] + envlist = standard-greeting + + [testenv:standard-greeting] + commands = + python -c 'print("Hello, world!")' + + [testenv:custom-greeting] + passenv = + NAME + commands = + python -c 'print("Hello, {env:NAME}!")' + """) + conf = config.envconfigs['standard-greeting'] + assert conf.commands == [ + ['python', '-c', 'print("Hello, world!")'], + ] + @pytest.mark.xfail(raises=AssertionError, reason="issue #301") def test_substitution_env_defaults_issue301(tmpdir, newconfig, monkeypatch): monkeypatch.setenv("IGNORE_STATIC_DEFAULT", "env") diff --git a/tox/config.py b/tox/config.py index 406cc5de..2d240152 100755 --- a/tox/config.py +++ b/tox/config.py @@ -787,7 +787,8 @@ class parseini: factors = set(name.split('-')) if section in self._cfg or factors <= known_factors: config.envconfigs[name] = \ - self.make_envconfig(name, section, reader._subs, config) + self.make_envconfig(name, section, reader._subs, config, + replace=name in config.envlist) all_develop = all(name in config.envconfigs and config.envconfigs[name].usedevelop @@ -803,7 +804,7 @@ class parseini: factors.update(*mapcat(_split_factor_expr, exprs)) return factors - def make_envconfig(self, name, section, subs, config): + def make_envconfig(self, name, section, subs, config, replace=True): factors = set(name.split('-')) reader = SectionReader(section, self._cfg, fallbacksections=["testenv"], factors=factors) @@ -818,7 +819,7 @@ class parseini: atype = env_attr.type if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"): meth = getattr(reader, "get" + atype) - res = meth(env_attr.name, env_attr.default) + res = meth(env_attr.name, env_attr.default, replace=replace) elif atype == "space-separated-list": res = reader.getlist(env_attr.name, sep=" ") elif atype == "line-list": @@ -937,9 +938,9 @@ class SectionReader: if _posargs: self.posargs = _posargs - def getpath(self, name, defaultpath): + def getpath(self, name, defaultpath, replace=True): toxinidir = self._subs['toxinidir'] - path = self.getstring(name, defaultpath) + path = self.getstring(name, defaultpath, replace=replace) if path is not None: return toxinidir.join(path, abs=True) @@ -949,12 +950,12 @@ class SectionReader: return [] return [x.strip() for x in s.split(sep) if x.strip()] - def getdict(self, name, default=None, sep="\n"): + def getdict(self, name, default=None, sep="\n", replace=True): value = self.getstring(name, None) return self._getdict(value, default=default, sep=sep) - def getdict_setenv(self, name, default=None, sep="\n"): - value = self.getstring(name, None, replace=True, crossonly=True) + def getdict_setenv(self, name, default=None, sep="\n", replace=True): + value = self.getstring(name, None, replace=replace, crossonly=True) definitions = self._getdict(value, default=default, sep=sep) self._setenv = SetenvDict(definitions, reader=self) return self._setenv @@ -971,8 +972,8 @@ class SectionReader: return d - def getbool(self, name, default=None): - s = self.getstring(name, default) + def getbool(self, name, default=None, replace=True): + s = self.getstring(name, default, replace=replace) if not s: s = default if s is None: @@ -989,12 +990,12 @@ class SectionReader: "boolean value %r needs to be 'True' or 'False'") return s - def getargvlist(self, name, default=""): + def getargvlist(self, name, default="", replace=True): s = self.getstring(name, default, replace=False) - return _ArgvlistReader.getargvlist(self, s) + return _ArgvlistReader.getargvlist(self, s, replace=replace) - def getargv(self, name, default=""): - return self.getargvlist(name, default)[0] + 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): x = None @@ -1136,7 +1137,7 @@ class Replacer: class _ArgvlistReader: @classmethod - def getargvlist(cls, reader, value): + def getargvlist(cls, reader, value, replace=True): """Parse ``commands`` argvlist multiline string. :param str name: Key name in a section. @@ -1161,7 +1162,7 @@ class _ArgvlistReader: replaced = reader._replace(current_command, crossonly=True) commands.extend(cls.getargvlist(reader, replaced)) else: - commands.append(cls.processcommand(reader, current_command)) + commands.append(cls.processcommand(reader, current_command, replace)) current_command = "" else: if current_command: @@ -1171,7 +1172,7 @@ class _ArgvlistReader: return commands @classmethod - def processcommand(cls, reader, command): + def processcommand(cls, reader, command, replace=True): posargs = getattr(reader, "posargs", "") posargs_string = list2cmdline([x for x in posargs if x]) @@ -1179,23 +1180,26 @@ class _ArgvlistReader: # appropriate to construct the new command string. This # string is then broken up into exec argv components using # shlex. - newcommand = "" - for word in CommandParser(command).words(): - if word == "{posargs}" or word == "[]": - newcommand += posargs_string - continue - elif word.startswith("{posargs:") and word.endswith("}"): - if posargs: + if replace: + newcommand = "" + for word in CommandParser(command).words(): + if word == "{posargs}" or word == "[]": newcommand += posargs_string continue - else: - word = word[9:-1] - new_arg = "" - new_word = reader._replace(word) - new_word = reader._replace(new_word) - new_word = new_word.replace('\\{', '{').replace('\\}', '}') - new_arg += new_word - newcommand += new_arg + elif word.startswith("{posargs:") and word.endswith("}"): + if posargs: + newcommand += posargs_string + continue + else: + word = word[9:-1] + new_arg = "" + new_word = reader._replace(word) + new_word = reader._replace(new_word) + new_word = new_word.replace('\\{', '{').replace('\\}', '}') + new_arg += new_word + newcommand += new_arg + else: + newcommand = command # Construct shlex object that will not escape any values, # use all values as is in argv. |
