Skip to content

Commit 649c6c0

Browse files
authored
Improve substitution recursion check (#1720)
Creates SubstitutionStackError and trace recursion from the beginning of substitution, allowing prevention of the recursion to halt sooner, and avoid None in error message. Related to #1716
1 parent c5a10d7 commit 649c6c0

File tree

3 files changed

+21
-19
lines changed

3 files changed

+21
-19
lines changed

src/tox/config/__init__.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def get(self, name, default=None):
391391
return os.environ.get(name, default)
392392
self._lookupstack.append(name)
393393
try:
394-
self.resolved[name] = res = self.reader._replace(val)
394+
self.resolved[name] = res = self.reader._replace(val, name="setenv")
395395
finally:
396396
self._lookupstack.pop()
397397
return res
@@ -1693,7 +1693,7 @@ def getbool(self, name, default=None, replace=True):
16931693

16941694
def getargvlist(self, name, default="", replace=True):
16951695
s = self.getstring(name, default, replace=False)
1696-
return _ArgvlistReader.getargvlist(self, s, replace=replace)
1696+
return _ArgvlistReader.getargvlist(self, s, replace=replace, name=name)
16971697

16981698
def getargv(self, name, default="", replace=True):
16991699
return self.getargvlist(name, default, replace=replace)[0]
@@ -1712,7 +1712,7 @@ def getargv_install_command(self, name, default="", replace=True):
17121712
if "{opts}" in s:
17131713
s = s.replace("{opts}", r"\{opts\}")
17141714

1715-
return _ArgvlistReader.getargvlist(self, s, replace=replace)[0]
1715+
return _ArgvlistReader.getargvlist(self, s, replace=replace, name=name)[0]
17161716

17171717
def getstring(self, name, default=None, replace=True, crossonly=False, no_fallback=False):
17181718
x = None
@@ -1775,6 +1775,7 @@ def _replace(self, value, name=None, section_name=None, crossonly=False):
17751775
return value
17761776

17771777
section_name = section_name if section_name else self.section_name
1778+
assert name
17781779
self._subststack.append((section_name, name))
17791780
try:
17801781
replaced = Replacer(self, crossonly=crossonly).do_replace(value)
@@ -1890,7 +1891,7 @@ def _substitute_from_other_section(self, key):
18901891
cfg = self.reader._cfg
18911892
if section in cfg and item in cfg[section]:
18921893
if (section, item) in self.reader._subststack:
1893-
raise ValueError(
1894+
raise tox.exception.SubstitutionStackError(
18941895
"{} already in {}".format((section, item), self.reader._subststack),
18951896
)
18961897
x = str(cfg[section][item])
@@ -1918,7 +1919,7 @@ def is_interactive():
19181919

19191920
class _ArgvlistReader:
19201921
@classmethod
1921-
def getargvlist(cls, reader, value, replace=True):
1922+
def getargvlist(cls, reader, value, replace=True, name=None):
19221923
"""Parse ``commands`` argvlist multiline string.
19231924
19241925
:param SectionReader reader: reader to be used.
@@ -1940,10 +1941,10 @@ def getargvlist(cls, reader, value, replace=True):
19401941
current_command += line
19411942

19421943
if is_section_substitution(current_command):
1943-
replaced = reader._replace(current_command, crossonly=True)
1944-
commands.extend(cls.getargvlist(reader, replaced))
1944+
replaced = reader._replace(current_command, crossonly=True, name=name)
1945+
commands.extend(cls.getargvlist(reader, replaced, name=name))
19451946
else:
1946-
commands.append(cls.processcommand(reader, current_command, replace))
1947+
commands.append(cls.processcommand(reader, current_command, replace, name=name))
19471948
current_command = ""
19481949
else:
19491950
if current_command:
@@ -1956,7 +1957,7 @@ def getargvlist(cls, reader, value, replace=True):
19561957
return commands
19571958

19581959
@classmethod
1959-
def processcommand(cls, reader, command, replace=True):
1960+
def processcommand(cls, reader, command, replace=True, name=None):
19601961
# Iterate through each word of the command substituting as
19611962
# appropriate to construct the new command string. This
19621963
# string is then broken up into exec argv components using
@@ -1969,8 +1970,8 @@ def processcommand(cls, reader, command, replace=True):
19691970
continue
19701971

19711972
new_arg = ""
1972-
new_word = reader._replace(word)
1973-
new_word = reader._replace(new_word)
1973+
new_word = reader._replace(word, name=name)
1974+
new_word = reader._replace(new_word, name=name)
19741975
new_word = Replacer._unescape(new_word)
19751976
new_arg += new_word
19761977
newcommand += new_arg

src/tox/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class ConfigError(Error):
5656
"""Error in tox configuration."""
5757

5858

59+
class SubstitutionStackError(ConfigError, ValueError):
60+
"""Error in tox configuration recursive substitution."""
61+
62+
5963
class UnsupportedInterpreter(Error):
6064
"""Signals an unsupported Interpreter."""
6165

tests/unit/config/test_config.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -438,10 +438,7 @@ class TestIniParserAgainstCommandsKey:
438438
"""Test parsing commands with substitutions"""
439439

440440
def test_command_substitution_recursion_error_same_section(self, newconfig):
441-
expected = (
442-
r"\('testenv:a', 'commands'\) already in "
443-
r"\[\('testenv:a', None\), \('testenv:a', 'commands'\)\]"
444-
)
441+
expected = r"\('testenv:a', 'commands'\) already in \[\('testenv:a', 'commands'\)\]"
445442
with pytest.raises(tox.exception.ConfigError, match=expected):
446443
newconfig(
447444
"""
@@ -452,9 +449,9 @@ def test_command_substitution_recursion_error_same_section(self, newconfig):
452449

453450
def test_command_substitution_recursion_error_other_section(self, newconfig):
454451
expected = (
455-
r"\('testenv:base', 'foo'\) already in "
456-
r"\[\('testenv:py27', None\), \('testenv:base', 'foo'\), "
457-
r"\('testenv:py27', 'commands'\)\]"
452+
r"\('testenv:py27', 'commands'\) already in "
453+
r"\[\('testenv:py27', 'commands'\), "
454+
r"\('testenv:base', 'foo'\)\]"
458455
)
459456
with pytest.raises(tox.exception.ConfigError, match=expected):
460457
newconfig(
@@ -472,7 +469,7 @@ def test_command_substitution_recursion_error_unnecessary(self, newconfig):
472469
# could be optimised away, or emit a warning, or give a custom error
473470
expected = (
474471
r"\('testenv:base', 'foo'\) already in "
475-
r"\[\('testenv:py27', None\), \('testenv:base', 'foo'\)\]"
472+
r"\[\('testenv:py27', 'commands'\), \('testenv:base', 'foo'\)\]"
476473
)
477474
with pytest.raises(tox.exception.ConfigError, match=expected):
478475
newconfig(

0 commit comments

Comments
 (0)