Skip to content

Commit b528048

Browse files
committed
Fix escaping brackets in setenv (tox-dev#1691)
1 parent b6c7ac6 commit b528048

File tree

5 files changed

+62
-9
lines changed

5 files changed

+62
-9
lines changed

docs/changelog/1690.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed regression in v3.20.0 that caused escaped curly braces in setenv
2+
to break usage of the variable elsewhere in tox.ini. - by :user:`jayvdb`

src/tox/config/__init__.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,7 @@ def get(self, name, default=None):
390390
return os.environ.get(name, default)
391391
self._lookupstack.append(name)
392392
try:
393-
res = self.reader._replace(val)
394-
res = res.replace("\\{", "{").replace("\\}", "}")
395-
self.resolved[name] = res
393+
self.resolved[name] = res = self.reader._replace(val)
396394
finally:
397395
self._lookupstack.pop()
398396
return res
@@ -410,6 +408,19 @@ def __setitem__(self, name, value):
410408
self.definitions[name] = value
411409
self.resolved[name] = value
412410

411+
def items(self):
412+
return ((name, self[name]) for name in self.definitions)
413+
414+
def export(self):
415+
# post-process items to avoid internal syntax/semantics
416+
# such as {} being escaped using \{\}, suitable for use with
417+
# os.environ .
418+
return {
419+
name: Replacer._unescape(value)
420+
for name, value in self.items()
421+
if value is not self._DUMMY
422+
}
423+
413424

414425
@tox.hookimpl
415426
def tox_addoption(parser):
@@ -1787,6 +1798,10 @@ def substitute_once(x):
17871798

17881799
return expanded
17891800

1801+
@staticmethod
1802+
def _unescape(s):
1803+
return s.replace("\\{", "{").replace("\\}", "}")
1804+
17901805
def _replace_match(self, match):
17911806
g = match.groupdict()
17921807
sub_value = g["substitution_value"]
@@ -1923,7 +1938,7 @@ def processcommand(cls, reader, command, replace=True):
19231938
new_arg = ""
19241939
new_word = reader._replace(word)
19251940
new_word = reader._replace(new_word)
1926-
new_word = new_word.replace("\\{", "{").replace("\\}", "}")
1941+
new_word = Replacer._unescape(new_word)
19271942
new_arg += new_word
19281943
newcommand += new_arg
19291944
else:

src/tox/venv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ def _get_os_environ(self, is_test_command=False):
493493
env = os.environ.copy()
494494

495495
# in any case we honor per-testenv setenv configuration
496-
env.update(self.envconfig.setenv)
496+
env.update(self.envconfig.setenv.export())
497497

498498
env["VIRTUAL_ENV"] = str(self.path)
499499
return env

tests/unit/config/test_config.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2120,9 +2120,12 @@ def test_curly_braces_in_setenv(self, newconfig):
21202120
[testenv]
21212121
setenv =
21222122
VAR = \{val\}
2123+
commands =
2124+
{env:VAR}
21232125
"""
21242126
configs = newconfig([], inisource).envconfigs
2125-
assert configs["python"].setenv["VAR"] == "{val}"
2127+
assert configs["python"].setenv["VAR"] == r"\{val\}"
2128+
assert configs["python"].commands[0] == ["{val}"]
21262129

21272130
def test_factor_use_not_checked(self, newconfig):
21282131
inisource = """
@@ -2731,13 +2734,22 @@ def test_setenv_env_file(self, newconfig, content, has_magic, tmp_path):
27312734
env_path,
27322735
),
27332736
).envconfigs["python"]
2737+
27342738
envs = env_config.setenv.definitions
2739+
27352740
assert envs["ALPHA"] == "1"
27362741
if has_magic:
27372742
assert envs["MAGIC"] == "yes"
27382743
else:
27392744
assert "MAGIC" not in envs
27402745

2746+
expected_vars = ["ALPHA", "PYTHONHASHSEED", "TOX_ENV_DIR", "TOX_ENV_NAME"]
2747+
if has_magic:
2748+
expected_vars = sorted(expected_vars + ["MAGIC"])
2749+
2750+
exported = env_config.setenv.export()
2751+
assert sorted(exported) == expected_vars
2752+
27412753

27422754
class TestIndexServer:
27432755
def test_indexserver(self, newconfig):

tests/unit/test_venv.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import py
55
import pytest
6+
from six import PY2
67

78
import tox
89
from tox.interpreters import NoInterpreterInfo
@@ -770,21 +771,36 @@ def test_pythonpath_empty(self, newmocksession, monkeypatch, caplog):
770771
assert "PYTHONPATH" not in pcalls[0].env
771772

772773

773-
def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch):
774+
def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch, tmp_path):
774775
monkeypatch.delenv("PYTHONPATH", raising=False)
775776
pkg = tmpdir.ensure("package.tar.gz")
776777
monkeypatch.setenv("X123", "123")
777778
monkeypatch.setenv("YY", "456")
779+
env_path = tmp_path / ".env"
780+
env_file_content = "ENV_FILE_VAR = file_value"
781+
env_path.write_text(env_file_content.decode() if PY2 else env_file_content)
782+
778783
config = newconfig(
779784
[],
780-
"""\
785+
r"""
786+
[base]
787+
base_var = base_value
788+
781789
[testenv:python]
782790
commands=python -V
783791
passenv = x123
784792
setenv =
785793
ENV_VAR = value
794+
ESCAPED_VAR = \{value\}
795+
ESCAPED_VAR2 = \\{value\\}
796+
BASE_VAR = {[base]base_var}
786797
PYTHONPATH = value
787-
""",
798+
TTY_VAR = {tty:ON_VALUE:OFF_VALUE}
799+
COLON = {:}
800+
REUSED_FILE_VAR = reused {env:ENV_FILE_VAR}
801+
file| %s
802+
"""
803+
% env_path,
788804
)
789805
mocksession._clearmocks()
790806
mocksession.new_config(config)
@@ -799,10 +815,18 @@ def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatc
799815
assert env is not None
800816
assert "ENV_VAR" in env
801817
assert env["ENV_VAR"] == "value"
818+
assert env["ESCAPED_VAR"] == "{value}"
819+
assert env["ESCAPED_VAR2"] == r"\{value\}"
820+
assert env["COLON"] == ";" if sys.platform == "win32" else ":"
821+
assert env["TTY_VAR"] == "OFF_VALUE"
822+
assert env["ENV_FILE_VAR"] == "file_value"
823+
assert env["REUSED_FILE_VAR"] == "reused file_value"
824+
assert env["BASE_VAR"] == "base_value"
802825
assert env["VIRTUAL_ENV"] == str(venv.path)
803826
assert env["X123"] == "123"
804827
assert "PYTHONPATH" in env
805828
assert env["PYTHONPATH"] == "value"
829+
806830
# all env variables are passed for installation
807831
assert pcalls[0].env["YY"] == "456"
808832
assert "YY" not in pcalls[1].env

0 commit comments

Comments
 (0)