Skip to content

Commit

Permalink
Un-escape backslashes before populating os.environ
Browse files Browse the repository at this point in the history
Related to tox-dev#1656
  • Loading branch information
jayvdb committed Oct 19, 2020
1 parent ce109b2 commit d62320f
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/changelog/1690.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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`
19 changes: 18 additions & 1 deletion src/tox/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/tox/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
30 changes: 27 additions & 3 deletions tests/unit/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import py
import pytest
from six import PY2

import tox
from tox.interpreters import NoInterpreterInfo
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit d62320f

Please sign in to comment.