From d62320fdd2212457e89ba4f8fd5d3d0513d4e201 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 17 Oct 2020 19:54:47 +0700 Subject: [PATCH] Un-escape backslashes before populating os.environ Related to https://github.com/tox-dev/tox/pull/1656 --- docs/changelog/1690.bugfix.rst | 2 ++ src/tox/config/__init__.py | 19 ++++++++++++++++++- src/tox/venv.py | 2 +- tests/unit/config/test_config.py | 9 +++++++++ tests/unit/test_venv.py | 30 +++++++++++++++++++++++++++--- 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/1690.bugfix.rst diff --git a/docs/changelog/1690.bugfix.rst b/docs/changelog/1690.bugfix.rst new file mode 100644 index 0000000000..c2a6f1c2ef --- /dev/null +++ b/docs/changelog/1690.bugfix.rst @@ -0,0 +1,2 @@ +Fixed regression in v3.20.0 that caused escaped curly braces in setenv +to break usage of the variable elsewhere in tox.ini. - by :user:`jayvdb` diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py index a10a4a6244..95f66136dc 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -408,6 +408,19 @@ def __setitem__(self, name, value): self.definitions[name] = value self.resolved[name] = value + def items(self): + return ((name, self[name]) for name in self.definitions) + + def export(self): + # post-process items to avoid internal syntax/semantics + # such as {} being escaped using \{\}, suitable for use with + # os.environ . + return { + name: Replacer._unescape(value) + for name, value in self.items() + if value is not self._DUMMY + } + @tox.hookimpl def tox_addoption(parser): @@ -1785,6 +1798,10 @@ def substitute_once(x): return expanded + @staticmethod + def _unescape(s): + return s.replace("\\{", "{").replace("\\}", "}") + def _replace_match(self, match): g = match.groupdict() sub_value = g["substitution_value"] @@ -1924,7 +1941,7 @@ def processcommand(cls, reader, command, replace=True): new_arg = "" new_word = reader._replace(word) new_word = reader._replace(new_word) - new_word = new_word.replace("\\{", "{").replace("\\}", "}") + new_word = Replacer._unescape(new_word) new_arg += new_word newcommand += new_arg else: diff --git a/src/tox/venv.py b/src/tox/venv.py index f2bef4884c..8d2d55b10e 100644 --- a/src/tox/venv.py +++ b/src/tox/venv.py @@ -493,7 +493,7 @@ def _get_os_environ(self, is_test_command=False): env = os.environ.copy() # in any case we honor per-testenv setenv configuration - env.update(self.envconfig.setenv) + env.update(self.envconfig.setenv.export()) env["VIRTUAL_ENV"] = str(self.path) return env diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index ca148638c8..e51872bd5a 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -2734,13 +2734,22 @@ def test_setenv_env_file(self, newconfig, content, has_magic, tmp_path): env_path, ), ).envconfigs["python"] + envs = env_config.setenv.definitions + assert envs["ALPHA"] == "1" if has_magic: assert envs["MAGIC"] == "yes" else: assert "MAGIC" not in envs + expected_vars = ["ALPHA", "PYTHONHASHSEED", "TOX_ENV_DIR", "TOX_ENV_NAME"] + if has_magic: + expected_vars = sorted(expected_vars + ["MAGIC"]) + + exported = env_config.setenv.export() + assert sorted(exported) == expected_vars + class TestIndexServer: def test_indexserver(self, newconfig): diff --git a/tests/unit/test_venv.py b/tests/unit/test_venv.py index 22e059cdbd..cc8eb1273b 100644 --- a/tests/unit/test_venv.py +++ b/tests/unit/test_venv.py @@ -3,6 +3,7 @@ import py import pytest +from six import PY2 import tox from tox.interpreters import NoInterpreterInfo @@ -770,21 +771,36 @@ def test_pythonpath_empty(self, newmocksession, monkeypatch, caplog): assert "PYTHONPATH" not in pcalls[0].env -def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch): +def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch, tmp_path): monkeypatch.delenv("PYTHONPATH", raising=False) pkg = tmpdir.ensure("package.tar.gz") monkeypatch.setenv("X123", "123") monkeypatch.setenv("YY", "456") + env_path = tmp_path / ".env" + env_file_content = "ENV_FILE_VAR = file_value" + env_path.write_text(env_file_content.decode() if PY2 else env_file_content) + config = newconfig( [], - """\ + r""" + [base] + base_var = base_value + [testenv:python] commands=python -V passenv = x123 setenv = ENV_VAR = value + ESCAPED_VAR = \{value\} + ESCAPED_VAR2 = \\{value\\} + BASE_VAR = {[base]base_var} PYTHONPATH = value - """, + TTY_VAR = {tty:ON_VALUE:OFF_VALUE} + COLON = {:} + REUSED_FILE_VAR = reused {env:ENV_FILE_VAR} + file| %s + """ + % env_path, ) mocksession._clearmocks() mocksession.new_config(config) @@ -799,10 +815,18 @@ def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatc assert env is not None assert "ENV_VAR" in env assert env["ENV_VAR"] == "value" + assert env["ESCAPED_VAR"] == "{value}" + assert env["ESCAPED_VAR2"] == r"\{value\}" + assert env["COLON"] == ";" if sys.platform == "win32" else ":" + assert env["TTY_VAR"] == "OFF_VALUE" + assert env["ENV_FILE_VAR"] == "file_value" + assert env["REUSED_FILE_VAR"] == "reused file_value" + assert env["BASE_VAR"] == "base_value" assert env["VIRTUAL_ENV"] == str(venv.path) assert env["X123"] == "123" assert "PYTHONPATH" in env assert env["PYTHONPATH"] == "value" + # all env variables are passed for installation assert pcalls[0].env["YY"] == "456" assert "YY" not in pcalls[1].env