Skip to content

Commit 4852262

Browse files
TOML set_env file support (#3478)
* [BUGFIX] TOML config interpreting set_env=file|... * Tests * Changelog entry * Added set_env 'file' option in TOML as dict * Testing set_env 'file' option in TOML as dict * Updating docs on set_env 'file' option (INI vs TOML) * Removing INI-specific check from TOML internal replacer function * Additional extension on docs, highlighting internal references * PR feedback Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net> --------- Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net> Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com> Co-authored-by: Bernát Gábor <bgabor8@bloomberg.net>
1 parent 1dac11f commit 4852262

File tree

5 files changed

+119
-12
lines changed

5 files changed

+119
-12
lines changed

docs/changelog/3474.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support ``set_env = { file = "conf{/}local.env"}`` for TOML format - by :user:`juditnovak`.

docs/config.rst

+37-2
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,43 @@ Base options
551551
.. conf::
552552
:keys: set_env, setenv
553553

554-
A dictionary of environment variables to set when running commands in the tox environment. Lines starting with a
555-
``file|`` prefix define the location of environment file.
554+
A dictionary of environment variables to set when running commands in the tox environment.
555+
556+
In addition, there is an option to include an existing environment file. See the different syntax for TOML and INI below.
557+
558+
.. tab:: TOML
559+
560+
.. code-block:: toml
561+
562+
[tool.tox.env_run_base]
563+
set_env = { file = "conf{/}local.env", TEST_TIMEOUT = 30 }
564+
565+
.. tab:: INI
566+
567+
.. code-block:: ini
568+
569+
[testenv]
570+
set_env = file|conf{/}local.env
571+
TEST_TIMEOUT = 30
572+
573+
574+
The env file path may include previously defined tox variables:
575+
576+
577+
.. tab:: TOML
578+
579+
.. code-block:: toml
580+
581+
[tool.tox.env_run_base]
582+
set_env = { file = "{env:variable}" }
583+
584+
.. tab:: INI
585+
586+
.. code-block:: ini
587+
588+
[testenv]
589+
set_env = file|{env:variable}
590+
556591
557592
.. note::
558593

src/tox/config/loader/toml/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

3+
import inspect
34
import logging
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Dict, Iterator, List, Mapping, TypeVar, cast
67

78
from tox.config.loader.api import ConfigLoadArgs, Loader, Override
9+
from tox.config.set_env import SetEnv
810
from tox.config.types import Command, EnvList
11+
from tox.report import HandledError
912

1013
from ._api import TomlTypes
1114
from ._replace import Unroll
@@ -63,7 +66,10 @@ def build( # noqa: PLR0913
6366
args: ConfigLoadArgs,
6467
) -> _T:
6568
exploded = Unroll(conf=conf, loader=self, args=args)(raw)
66-
return self.to(exploded, of_type, factory)
69+
result = self.to(exploded, of_type, factory)
70+
if inspect.isclass(of_type) and issubclass(of_type, SetEnv):
71+
result.use_replacer(lambda c, s: c, args=args) # type: ignore[attr-defined] # noqa: ARG005
72+
return result
6773

6874
def found_keys(self) -> set[str]:
6975
return set(self.content.keys()) - self._unused_exclude
@@ -107,5 +113,6 @@ def to_env_list(value: TomlTypes) -> EnvList:
107113

108114

109115
__all__ = [
116+
"HandledError",
110117
"TomlLoader",
111118
]

src/tox/config/set_env.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class SetEnv:
14-
def __init__( # noqa: C901
14+
def __init__( # noqa: C901, PLR0912
1515
self, raw: str | dict[str, str] | list[dict[str, str]], name: str, env_name: str | None, root: Path
1616
) -> None:
1717
self.changed = False
@@ -25,13 +25,16 @@ def __init__( # noqa: C901
2525

2626
if isinstance(raw, dict):
2727
self._raw = raw
28+
if "file" in raw: # environment files to be handled later
29+
self._env_files.append(raw["file"])
30+
self._raw.pop("file")
2831
return
2932
if isinstance(raw, list):
3033
self._raw = reduce(lambda a, b: {**a, **b}, raw)
3134
return
3235
for line in raw.splitlines(): # noqa: PLR1702
3336
if line.strip():
34-
if line.startswith("file|"):
37+
if line.startswith("file|"): # environment files to be handled later
3538
self._env_files.append(line[len("file|") :])
3639
else:
3740
try:

tests/config/test_set_env.py

+68-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4-
from typing import TYPE_CHECKING, Any
4+
from typing import TYPE_CHECKING, Any, Literal
55
from unittest.mock import ANY
66

77
import pytest
@@ -51,19 +51,30 @@ def test_set_env_bad_line() -> None:
5151
SetEnv("A", "py", "py", Path())
5252

5353

54+
ConfigFileFormat = Literal["ini", "toml"]
55+
56+
5457
class EvalSetEnv(Protocol):
5558
def __call__(
5659
self,
57-
tox_ini: str,
60+
config: str,
61+
*,
62+
of_type: ConfigFileFormat = "ini",
5863
extra_files: dict[str, Any] | None = ...,
5964
from_cwd: Path | None = ...,
6065
) -> SetEnv: ...
6166

6267

6368
@pytest.fixture
6469
def eval_set_env(tox_project: ToxProjectCreator) -> EvalSetEnv:
65-
def func(tox_ini: str, extra_files: dict[str, Any] | None = None, from_cwd: Path | None = None) -> SetEnv:
66-
prj = tox_project({"tox.ini": tox_ini, **(extra_files or {})})
70+
def func(
71+
config: str,
72+
*,
73+
of_type: ConfigFileFormat = "ini",
74+
extra_files: dict[str, Any] | None = None,
75+
from_cwd: Path | None = None,
76+
) -> SetEnv:
77+
prj = tox_project({f"tox.{of_type}": config, **(extra_files or {})})
6778
result = prj.run("c", "-k", "set_env", "-e", "py", from_cwd=None if from_cwd is None else prj.path / from_cwd)
6879
result.assert_success()
6980
set_env: SetEnv = result.env_conf("py")["set_env"]
@@ -149,7 +160,20 @@ def test_set_env_honor_override(eval_set_env: EvalSetEnv) -> None:
149160
assert set_env.load("PIP_DISABLE_PIP_VERSION_CHECK") == "0"
150161

151162

152-
def test_set_env_environment_file(eval_set_env: EvalSetEnv) -> None:
163+
@pytest.mark.parametrize(
164+
("of_type", "config"),
165+
[
166+
pytest.param("ini", "[testenv]\npackage=skip\nset_env=file|A{/}a.txt\nchange_dir=C", id="ini"),
167+
pytest.param("toml", '[env_run_base]\npackage="skip"\nset_env={file="A{/}a.txt"}\nchange_dir="C"', id="toml"),
168+
pytest.param("ini", "[testenv]\npackage=skip\nset_env=file|{env:env_file}\nchange_dir=C", id="ini-env"),
169+
pytest.param(
170+
"toml", '[env_run_base]\npackage="skip"\nset_env={file="{env:env_file}"}\nchange_dir="C"', id="toml-env"
171+
),
172+
],
173+
)
174+
def test_set_env_environment_file(
175+
of_type: ConfigFileFormat, config: str, eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch
176+
) -> None:
153177
env_file = """
154178
A=1
155179
B= 2
@@ -158,9 +182,10 @@ def test_set_env_environment_file(eval_set_env: EvalSetEnv) -> None:
158182
E = "1"
159183
F =
160184
"""
185+
monkeypatch.setenv("env_file", "A{/}a.txt")
186+
161187
extra = {"A": {"a.txt": env_file}, "B": None, "C": None}
162-
ini = "[testenv]\npackage=skip\nset_env=file|A{/}a.txt\nchange_dir=C"
163-
set_env = eval_set_env(ini, extra_files=extra, from_cwd=Path("B"))
188+
set_env = eval_set_env(config, of_type=of_type, extra_files=extra, from_cwd=Path("B"))
164189
content = {k: set_env.load(k) for k in set_env}
165190
assert content == {
166191
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
@@ -174,6 +199,42 @@ def test_set_env_environment_file(eval_set_env: EvalSetEnv) -> None:
174199
}
175200

176201

202+
@pytest.mark.parametrize(
203+
("of_type", "config"),
204+
[
205+
pytest.param("ini", "[testenv]\npackage=skip\nset_env=file|A{/}a.txt\n X=y\nchange_dir=C", id="ini"),
206+
pytest.param(
207+
"toml", '[env_run_base]\npackage="skip"\nset_env={file="A{/}a.txt", X="y"}\nchange_dir="C"', id="toml"
208+
),
209+
pytest.param("ini", "[testenv]\npackage=skip\nset_env=file|{env:env_file}\n X=y\nchange_dir=C", id="ini-env"),
210+
pytest.param(
211+
"toml",
212+
'[env_run_base]\npackage="skip"\nset_env={file="{env:env_file}", X="y"}\nchange_dir="C"',
213+
id="toml-env",
214+
),
215+
],
216+
)
217+
def test_set_env_environment_file_combined_with_normal_setting(
218+
of_type: ConfigFileFormat, config: str, eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch
219+
) -> None:
220+
env_file = """
221+
A=1
222+
"""
223+
# Monkeypatch only used for some of the parameters
224+
monkeypatch.setenv("env_file", "A{/}a.txt")
225+
226+
extra = {"A": {"a.txt": env_file}, "B": None, "C": None}
227+
set_env = eval_set_env(config, of_type=of_type, extra_files=extra, from_cwd=Path("B"))
228+
content = {k: set_env.load(k) for k in set_env}
229+
assert content == {
230+
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
231+
"PYTHONHASHSEED": ANY,
232+
"A": "1",
233+
"X": "y",
234+
"PYTHONIOENCODING": "utf-8",
235+
}
236+
237+
177238
def test_set_env_environment_file_missing(tox_project: ToxProjectCreator) -> None:
178239
project = tox_project({"tox.ini": "[testenv]\npackage=skip\nset_env=file|magic.txt"})
179240
result = project.run("r")

0 commit comments

Comments
 (0)