Skip to content

Commit b56738c

Browse files
authored
Fix substitution problems introduced in 2.8 (#599)
* #595 fix by updating implementation of #515 The way #515 was implemented did not play nicely with env var substitution. This might render the former additions completely or partially obsolete but I did not check this yet. First let's fix that bug. * fix flakes (cherry picked from commit e902f04) * #595 fix by updating implementation of #515 The way #515 was implemented did not play nicely with env var substitution. This might render the former additions completely or partially obsolete but I did not check this yet. First let's fix that bug. Instead of crashing early on testenv creation if the substitution the missing substitution keys are added to a list attached to TestenvConfig. This way it is not necessary anymore to provide everything needed to resolve all testenvs but instead only what is needed to resolve all testenvs that are part of this restrun. Implementation notes: The first implementation used a module level mapping from envname to missing substitutions, which was ugly. Instead the information is bubbled up as an exception where it can be handled the way it is needed in the contexts where the information can be either translated into a crashing error or attached to the object which should carry the information (in this case TestenvConfig). Atm a missing substitution in a testenv is not being left empty but filled with a special value 'TOX_MISSING_SUBSTITUTION', this is not exactly necessary, but I don't feel comfortable enough yet to leave it empty, this can help with debugging potential problems that might arise from this change. * #595 missed a bit Crash only if actually trying to run a testenv with missing substitutions * fix error on execution Instead of crashing the whole thing again - only later, do it the right way by setting venv.status with the error to be reported later. * #595 tests and last touches * catch MissingSubstition from all method calls for consistency * set status and stop execution in setupvenv if substutions are missing to prevent errors occuring there with missing substitutions
1 parent 9a22ecc commit b56738c

File tree

5 files changed

+78
-54
lines changed

5 files changed

+78
-54
lines changed

tests/test_config.py

+13-11
Original file line numberDiff line numberDiff line change
@@ -444,18 +444,20 @@ def test_getdict(self, tmpdir, newconfig):
444444
x = reader.getdict("key3", {1: 2})
445445
assert x == {1: 2}
446446

447-
def test_getstring_environment_substitution(self, monkeypatch, newconfig):
448-
monkeypatch.setenv("KEY1", "hello")
449-
config = newconfig("""
450-
[section]
451-
key1={env:KEY1}
452-
key2={env:KEY2}
453-
""")
454-
reader = SectionReader("section", config._cfg)
455-
x = reader.getstring("key1")
456-
assert x == "hello"
447+
def test_normal_env_sub_works(self, monkeypatch, newconfig):
448+
monkeypatch.setenv("VAR", "hello")
449+
config = newconfig("[section]\nkey={env:VAR}")
450+
assert SectionReader("section", config._cfg).getstring("key") == "hello"
451+
452+
def test_missing_env_sub_raises_config_error_in_non_testenv(self, newconfig):
453+
config = newconfig("[section]\nkey={env:VAR}")
457454
with pytest.raises(tox.exception.ConfigError):
458-
reader.getstring("key2")
455+
SectionReader("section", config._cfg).getstring("key")
456+
457+
def test_missing_env_sub_populates_missing_subs(self, newconfig):
458+
config = newconfig("[testenv:foo]\ncommands={env:VAR}")
459+
print(SectionReader("section", config._cfg).getstring("commands"))
460+
assert config.envconfigs['foo'].missing_subs == ['VAR']
459461

460462
def test_getstring_environment_substitution_with_default(self, monkeypatch, newconfig):
461463
monkeypatch.setenv("KEY1", "hello")

tests/test_z_cmdline.py

+7
Original file line numberDiff line numberDiff line change
@@ -871,3 +871,10 @@ def test_envtmpdir(initproj, cmd):
871871

872872
result = cmd.run("tox")
873873
assert not result.ret
874+
875+
876+
def test_missing_env_fails(initproj, cmd):
877+
initproj("foo", filedefs={'tox.ini': "[testenv:foo]\ncommands={env:VAR}"})
878+
result = cmd.run("tox")
879+
assert result.ret == 1
880+
result.stdout.fnmatch_lines(["*foo: unresolvable substitution(s): 'VAR'*"])

tox/__init__.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ class Error(Exception):
1414
def __str__(self):
1515
return "%s: %s" % (self.__class__.__name__, self.args[0])
1616

17+
class MissingSubstitution(Exception):
18+
FLAG = 'TOX_MISSING_SUBSTITUTION'
19+
"""placeholder for debugging configurations"""
20+
def __init__(self, name):
21+
self.name = name
22+
1723
class ConfigError(Error):
1824
""" error in tox configuration. """
1925
class UnsupportedInterpreter(Error):
20-
"signals an unsupported Interpreter"
26+
"""signals an unsupported Interpreter"""
2127
class InterpreterNotFound(Error):
22-
"signals that an interpreter could not be found"
28+
"""signals that an interpreter could not be found"""
2329
class InvocationError(Error):
2430
""" an error while invoking a script. """
2531
class MissingFile(Error):
@@ -35,4 +41,5 @@ def __init__(self, message):
3541
self.message = message
3642
super(exception.MinVersionError, self).__init__(message)
3743

44+
3845
from tox.session import main as cmdline # noqa

tox/config.py

+43-41
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,13 @@ def __init__(self, envname, config, factors, reader):
609609
#: set of factors
610610
self.factors = factors
611611
self._reader = reader
612+
self.missing_subs = []
613+
"""Holds substitutions that could not be resolved.
614+
615+
Pre 2.8.1 missing substitutions crashed with a ConfigError although this would not be a
616+
problem if the env is not part of the current testrun. So we need to remember this and
617+
check later when the testenv is actually run and crash only then.
618+
"""
612619

613620
def get_envbindir(self):
614621
""" path to directory where scripts/binaries reside. """
@@ -791,9 +798,7 @@ def __init__(self, config, inipath):
791798
section = testenvprefix + name
792799
factors = set(name.split('-'))
793800
if section in self._cfg or factors <= known_factors:
794-
config.envconfigs[name] = \
795-
self.make_envconfig(name, section, reader._subs, config,
796-
replace=name in config.envlist)
801+
config.envconfigs[name] = self.make_envconfig(name, section, reader._subs, config)
797802

798803
all_develop = all(name in config.envconfigs
799804
and config.envconfigs[name].usedevelop
@@ -813,33 +818,31 @@ def make_envconfig(self, name, section, subs, config, replace=True):
813818
factors = set(name.split('-'))
814819
reader = SectionReader(section, self._cfg, fallbacksections=["testenv"],
815820
factors=factors)
816-
vc = TestenvConfig(config=config, envname=name, factors=factors, reader=reader)
817-
reader.addsubstitutions(**subs)
818-
reader.addsubstitutions(envname=name)
819-
reader.addsubstitutions(envbindir=vc.get_envbindir,
820-
envsitepackagesdir=vc.get_envsitepackagesdir,
821-
envpython=vc.get_envpython)
822-
821+
tc = TestenvConfig(name, config, factors, reader)
822+
reader.addsubstitutions(
823+
envname=name, envbindir=tc.get_envbindir, envsitepackagesdir=tc.get_envsitepackagesdir,
824+
envpython=tc.get_envpython, **subs)
823825
for env_attr in config._testenv_attr:
824826
atype = env_attr.type
825-
if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"):
826-
meth = getattr(reader, "get" + atype)
827-
res = meth(env_attr.name, env_attr.default, replace=replace)
828-
elif atype == "space-separated-list":
829-
res = reader.getlist(env_attr.name, sep=" ")
830-
elif atype == "line-list":
831-
res = reader.getlist(env_attr.name, sep="\n")
832-
else:
833-
raise ValueError("unknown type %r" % (atype,))
834-
835-
if env_attr.postprocess:
836-
res = env_attr.postprocess(testenv_config=vc, value=res)
837-
setattr(vc, env_attr.name, res)
838-
827+
try:
828+
if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"):
829+
meth = getattr(reader, "get" + atype)
830+
res = meth(env_attr.name, env_attr.default, replace=replace)
831+
elif atype == "space-separated-list":
832+
res = reader.getlist(env_attr.name, sep=" ")
833+
elif atype == "line-list":
834+
res = reader.getlist(env_attr.name, sep="\n")
835+
else:
836+
raise ValueError("unknown type %r" % (atype,))
837+
if env_attr.postprocess:
838+
res = env_attr.postprocess(testenv_config=tc, value=res)
839+
except tox.exception.MissingSubstitution as e:
840+
tc.missing_subs.append(e.name)
841+
res = e.FLAG
842+
setattr(tc, env_attr.name, res)
839843
if atype in ("path", "string"):
840844
reader.addsubstitutions(**{env_attr.name: res})
841-
842-
return vc
845+
return tc
843846

844847
def _getenvdata(self, reader):
845848
envstr = self.config.option.env \
@@ -961,8 +964,7 @@ def getdict(self, name, default=None, sep="\n", replace=True):
961964

962965
def getdict_setenv(self, name, default=None, sep="\n", replace=True):
963966
value = self.getstring(name, None, replace=replace, crossonly=True)
964-
definitions = self._getdict(value, default=default, sep=sep,
965-
replace=replace)
967+
definitions = self._getdict(value, default=default, sep=sep, replace=replace)
966968
self._setenv = SetenvDict(definitions, reader=self)
967969
return self._setenv
968970

@@ -1042,9 +1044,15 @@ def _replace(self, value, name=None, section_name=None, crossonly=False):
10421044
section_name = section_name if section_name else self.section_name
10431045
self._subststack.append((section_name, name))
10441046
try:
1045-
return Replacer(self, crossonly=crossonly).do_replace(value)
1046-
finally:
1047+
replaced = Replacer(self, crossonly=crossonly).do_replace(value)
10471048
assert self._subststack.pop() == (section_name, name)
1049+
except tox.exception.MissingSubstitution:
1050+
if not section_name.startswith(testenvprefix):
1051+
raise tox.exception.ConfigError(
1052+
"substitution env:%r: unknown or recursive definition in "
1053+
"section %r." % (value, section_name))
1054+
raise
1055+
return replaced
10481056

10491057

10501058
class Replacer:
@@ -1112,20 +1120,14 @@ def _replace_match(self, match):
11121120
def _replace_env(self, match):
11131121
envkey = match.group('substitution_value')
11141122
if not envkey:
1115-
raise tox.exception.ConfigError(
1116-
'env: requires an environment variable name')
1117-
1123+
raise tox.exception.ConfigError('env: requires an environment variable name')
11181124
default = match.group('default_value')
1119-
11201125
envvalue = self.reader.get_environ_value(envkey)
1121-
if envvalue is None:
1122-
if default is None:
1123-
raise tox.exception.ConfigError(
1124-
"substitution env:%r: unknown environment variable %r "
1125-
" or recursive definition." %
1126-
(envkey, envkey))
1126+
if envvalue is not None:
1127+
return envvalue
1128+
if default is not None:
11271129
return default
1128-
return envvalue
1130+
raise tox.exception.MissingSubstitution(envkey)
11291131

11301132
def _substitute_from_other_section(self, key):
11311133
if key.startswith("[") and "]" in key:

tox/session.py

+6
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ def make_emptydir(self, path):
440440
path.ensure(dir=1)
441441

442442
def setupenv(self, venv):
443+
if venv.envconfig.missing_subs:
444+
venv.status = (
445+
"unresolvable substitution(s): %s. "
446+
"Environment variables are missing or defined recursively." %
447+
(','.join(["'%s'" % m for m in venv.envconfig.missing_subs])))
448+
return
443449
if not venv.matching_platform():
444450
venv.status = "platform mismatch"
445451
return # we simply omit non-matching platforms

0 commit comments

Comments
 (0)