summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndre Caron <andre.caron@ubisoft.com>2017-05-18 13:54:22 -0400
committerAndre Caron <andre.caron@ubisoft.com>2017-05-18 15:53:03 -0400
commit10b0d6c12ce041b4fe588c194ac9d80d82bd8d98 (patch)
tree0b002aa5d18222693014e5793511e37170cdda84
parent2b2a51f8cd845fb6df3f87bb6e83f3e9cc72ab28 (diff)
downloadtox-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--CHANGELOG2
-rw-r--r--tests/test_config.py20
-rwxr-xr-xtox/config.py68
3 files changed, 58 insertions, 32 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 04f64159..82f52fa3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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.