Skip to content

Commit d62320f

Browse files
committed
Un-escape backslashes before populating os.environ
Related to #1656
1 parent ce109b2 commit d62320f

File tree

5 files changed

+57
-5
lines changed

5 files changed

+57
-5
lines changed

docs/changelog/1690.bugfix.rst

+2
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

+18-1
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,19 @@ def __setitem__(self, name, value):
408408
self.definitions[name] = value
409409
self.resolved[name] = value
410410

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+
411424

412425
@tox.hookimpl
413426
def tox_addoption(parser):
@@ -1785,6 +1798,10 @@ def substitute_once(x):
17851798

17861799
return expanded
17871800

1801+
@staticmethod
1802+
def _unescape(s):
1803+
return s.replace("\\{", "{").replace("\\}", "}")
1804+
17881805
def _replace_match(self, match):
17891806
g = match.groupdict()
17901807
sub_value = g["substitution_value"]
@@ -1924,7 +1941,7 @@ def processcommand(cls, reader, command, replace=True):
19241941
new_arg = ""
19251942
new_word = reader._replace(word)
19261943
new_word = reader._replace(new_word)
1927-
new_word = new_word.replace("\\{", "{").replace("\\}", "}")
1944+
new_word = Replacer._unescape(new_word)
19281945
new_arg += new_word
19291946
newcommand += new_arg
19301947
else:

src/tox/venv.py

+1-1
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

+9
Original file line numberDiff line numberDiff line change
@@ -2734,13 +2734,22 @@ def test_setenv_env_file(self, newconfig, content, has_magic, tmp_path):
27342734
env_path,
27352735
),
27362736
).envconfigs["python"]
2737+
27372738
envs = env_config.setenv.definitions
2739+
27382740
assert envs["ALPHA"] == "1"
27392741
if has_magic:
27402742
assert envs["MAGIC"] == "yes"
27412743
else:
27422744
assert "MAGIC" not in envs
27432745

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+
27442753

27452754
class TestIndexServer:
27462755
def test_indexserver(self, newconfig):

tests/unit/test_venv.py

+27-3
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)