Skip to content

Commit 957f2f8

Browse files
realitycheckgaborbernatpre-commit-ci[bot]
authored
Fix for tox4 regression issue with setenv file and substitutions (#2435) (#3521)
Co-authored-by: Bernát Gábor <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5f07ba8 commit 957f2f8

File tree

3 files changed

+72
-8
lines changed

3 files changed

+72
-8
lines changed

docs/changelog/2435.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a regression issue related to inability to use ``file|`` substitution option in nested ``set_env`` sections of ``ini`` configurations since tox4 update.

src/tox/config/set_env.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def __init__( # noqa: C901, PLR0912
3434
return
3535
for line in raw.splitlines(): # noqa: PLR1702
3636
if line.strip():
37-
if line.startswith("file|"): # environment files to be handled later
38-
self._env_files.append(line[len("file|") :])
37+
if self._is_file_line(line):
38+
self._env_files.append(self._parse_file_line(line))
3939
else:
4040
try:
4141
key, value = self._extract_key_value(line)
@@ -52,12 +52,20 @@ def __init__( # noqa: C901, PLR0912
5252
else:
5353
self._raw[key] = value
5454

55+
@staticmethod
56+
def _is_file_line(line: str) -> bool:
57+
return line.startswith("file|")
58+
59+
@staticmethod
60+
def _parse_file_line(line: str) -> str:
61+
return line[len("file|") :]
62+
5563
def use_replacer(self, value: Replacer, args: ConfigLoadArgs) -> None:
5664
self._replacer = value
5765
for filename in self._env_files:
58-
self._read_env_file(filename, args)
66+
self._raw.update(self._stream_env_file(filename, args))
5967

60-
def _read_env_file(self, filename: str, args: ConfigLoadArgs) -> None:
68+
def _stream_env_file(self, filename: str, args: ConfigLoadArgs) -> Iterator[tuple[str, str]]:
6169
# Our rules in the documentation, some upstream environment file rules (we follow mostly the docker one):
6270
# - https://www.npmjs.com/package/dotenv#rules
6371
# - https://docs.docker.com/compose/env-file/
@@ -70,8 +78,7 @@ def _read_env_file(self, filename: str, args: ConfigLoadArgs) -> None:
7078
env_line = env_line.strip() # noqa: PLW2901
7179
if not env_line or env_line.startswith("#"):
7280
continue
73-
key, value = self._extract_key_value(env_line)
74-
self._raw[key] = value
81+
yield self._extract_key_value(env_line)
7582

7683
@staticmethod
7784
def _extract_key_value(line: str) -> tuple[str, str]:
@@ -100,10 +107,18 @@ def __iter__(self) -> Iterator[str]:
100107
# start with the materialized ones, maybe we don't need to materialize the raw ones
101108
yield from self._materialized.keys()
102109
yield from list(self._raw.keys()) # iterating over this may trigger materialization and change the dict
110+
args = ConfigLoadArgs([], self._name, self._env_name)
103111
while self._needs_replacement:
104112
line = self._needs_replacement.pop(0)
105-
expanded_line = self._replacer(line, ConfigLoadArgs([], self._name, self._env_name))
106-
sub_raw = dict(self._extract_key_value(sub_line) for sub_line in expanded_line.splitlines() if sub_line)
113+
expanded_line = self._replacer(line, args)
114+
sub_raw: dict[str, str] = {}
115+
for sub_line in filter(None, expanded_line.splitlines()):
116+
if not self._is_file_line(sub_line):
117+
sub_raw.__setitem__(*self._extract_key_value(sub_line))
118+
else:
119+
for key, value in self._stream_env_file(self._parse_file_line(sub_line), args):
120+
if key not in self._raw:
121+
sub_raw[key] = value # noqa: PERF403
107122
self._raw.update(sub_raw)
108123
self.changed = True # loading while iterating can cause these values to be missed
109124
yield from sub_raw.keys()

tests/config/test_set_env.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,51 @@ def test_set_env_environment_file_missing(tox_project: ToxProjectCreator) -> Non
240240
result = project.run("r")
241241
result.assert_failed()
242242
assert f"py: failed with {project.path / 'magic.txt'} does not exist for set_env" in result.out
243+
244+
245+
# https://github.com/tox-dev/tox/issues/2435
246+
def test_set_env_environment_with_file_and_expanded_substitution(
247+
tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch
248+
) -> None:
249+
conf = {
250+
"tox.ini": """
251+
[tox]
252+
envlist =
253+
check
254+
255+
[testenv]
256+
setenv =
257+
file|.env
258+
PRECENDENCE_TEST_1=1_expanded_precedence
259+
260+
[testenv:check]
261+
setenv =
262+
{[testenv]setenv}
263+
PRECENDENCE_TEST_1=1_self_precedence
264+
PRECENDENCE_TEST_2=2_self_precedence
265+
""",
266+
".env": """
267+
PRECENDENCE_TEST_1=1_file_precedence
268+
PRECENDENCE_TEST_2=2_file_precedence
269+
PRECENDENCE_TEST_3=3_file_precedence
270+
""",
271+
}
272+
monkeypatch.setenv("env_file", ".env")
273+
project = tox_project(conf)
274+
275+
result = project.run("c", "-k", "set_env", "-e", "check")
276+
result.assert_success()
277+
set_env = result.env_conf("check")["set_env"]
278+
content = {k: set_env.load(k) for k in set_env}
279+
assert content == {
280+
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
281+
"PYTHONHASHSEED": ANY,
282+
"PYTHONIOENCODING": "utf-8",
283+
"PRECENDENCE_TEST_1": "1_expanded_precedence",
284+
"PRECENDENCE_TEST_2": "2_self_precedence",
285+
"PRECENDENCE_TEST_3": "3_file_precedence",
286+
}
287+
288+
result = project.run("r", "-e", "check")
289+
result.assert_success()
290+
assert "check: OK" in result.out

0 commit comments

Comments
 (0)