Skip to content

Commit ee45435

Browse files
committed
Add API for substitution and refs
Signed-off-by: Bernát Gábor <[email protected]>
1 parent edd352e commit ee45435

File tree

4 files changed

+201
-2
lines changed

4 files changed

+201
-2
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from pathlib import Path
44
from typing import TYPE_CHECKING, Dict, Iterator, List, Mapping, Set, TypeVar, cast
55

6-
from tox.config.loader.api import Loader, Override
6+
from tox.config.loader.api import ConfigLoadArgs, Loader, Override
77
from tox.config.types import Command, EnvList
88

99
from ._api import TomlTypes
10+
from ._replace import unroll_refs_and_apply_substitutions
1011
from ._validate import validate
1112

1213
if TYPE_CHECKING:
14+
from tox.config.loader.convert import Factory
1315
from tox.config.loader.section import Section
1416
from tox.config.main import Config
1517

@@ -37,6 +39,18 @@ def __repr__(self) -> str:
3739
def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> TomlTypes: # noqa: ARG002
3840
return self.content[key]
3941

42+
def build( # noqa: PLR0913
43+
self,
44+
key: str, # noqa: ARG002
45+
of_type: type[_T],
46+
factory: Factory[_T],
47+
conf: Config | None,
48+
raw: TomlTypes,
49+
args: ConfigLoadArgs,
50+
) -> _T:
51+
raw = unroll_refs_and_apply_substitutions(conf=conf, loader=self, value=raw, args=args)
52+
return self.to(raw, of_type, factory)
53+
4054
def found_keys(self) -> set[str]:
4155
return set(self.content.keys()) - self._unused_exclude
4256

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Final
4+
5+
if TYPE_CHECKING:
6+
from tox.config.loader.api import ConfigLoadArgs
7+
from tox.config.loader.toml import TomlLoader
8+
from tox.config.main import Config
9+
10+
from ._api import TomlTypes
11+
12+
MAX_REPLACE_DEPTH: Final[int] = 100
13+
14+
15+
class MatchRecursionError(ValueError):
16+
"""Could not stabilize on replacement value."""
17+
18+
19+
def unroll_refs_and_apply_substitutions(
20+
conf: Config | None,
21+
loader: TomlLoader,
22+
value: TomlTypes,
23+
args: ConfigLoadArgs,
24+
depth: int = 0,
25+
) -> TomlTypes:
26+
"""Replace all active tokens within value according to the config."""
27+
if depth > MAX_REPLACE_DEPTH:
28+
msg = f"Could not expand {value} after recursing {depth} frames"
29+
raise MatchRecursionError(msg)
30+
31+
if isinstance(value, str):
32+
pass # apply string substitution here
33+
elif isinstance(value, (int, float, bool)):
34+
pass # no reference or substitution possible
35+
elif isinstance(value, list):
36+
# need to inspect every entry of the list to check for reference.
37+
res_list: list[TomlTypes] = []
38+
for val in value: # apply replacement for every entry
39+
got = unroll_refs_and_apply_substitutions(conf, loader, val, args, depth + 1)
40+
res_list.append(got)
41+
value = res_list
42+
elif isinstance(value, dict):
43+
# need to inspect every entry of the list to check for reference.
44+
res_dict: dict[str, TomlTypes] = {}
45+
for key, val in value.items(): # apply replacement for every entry
46+
got = unroll_refs_and_apply_substitutions(conf, loader, val, args, depth + 1)
47+
res_dict[key] = got
48+
value = res_dict
49+
return value
50+
51+
52+
__all__ = [
53+
"unroll_refs_and_apply_substitutions",
54+
]

tests/config/source/test_toml_tox.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ def test_config_in_toml_extra(tox_project: ToxProjectCreator) -> None:
6363

6464
def test_config_in_toml_replace(tox_project: ToxProjectCreator) -> None:
6565
project = tox_project({"tox.toml": '[env_run_base]\ndescription = "Magic in {env_name}"'})
66-
outcome = project.run("c", "-k", "commands")
66+
outcome = project.run("c", "-k", "description")
6767
outcome.assert_success()

tox.toml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
requires = ["tox>=4.19"]
2+
env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "cov", "type", "docs", "pkg_meta"]
3+
skip_missing_interpreters = true
4+
5+
[env_run_base]
6+
description = "run the tests with pytest under {env_name}"
7+
package = "wheel"
8+
wheel_build_env = ".pkg"
9+
extras = ["testing"]
10+
pass_env = ["PYTEST_*", "SSL_CERT_FILE"]
11+
set_env.COVERAGE_FILE = { type = "env", name = "COVERAGE_FILE", default = "{work_dir}{/}.coverage.{env_name}" }
12+
set_env.COVERAGE_FILECOVERAGE_PROCESS_START = "{tox_root}{/}pyproject.toml"
13+
commands = [
14+
[
15+
"pytest",
16+
{ type = "posargs", default = [
17+
"--junitxml",
18+
"{work_dir}{/}junit.{env_name}.xml",
19+
"--cov",
20+
"{env_site_packages_dir}{/}tox",
21+
"--cov",
22+
"{tox_root}{/}tests",
23+
"--cov-config={tox_root}{/}pyproject.toml",
24+
"-no-cov-on-fail",
25+
"--cov-report",
26+
"term-missing:skip-covered",
27+
"--cov-context=test",
28+
"--cov-report",
29+
"html:{env_tmp_dir}{/}htmlcov",
30+
"--cov-report",
31+
"xml:{work_dir}{/}coverage.{env_name}.xml",
32+
"-n",
33+
{ type = "env", name = "PYTEST_XDIST_AUTO_NUM_WORKERS", default = "auto" },
34+
"tests",
35+
"--durations",
36+
"15",
37+
"--run-integration",
38+
] },
39+
],
40+
[
41+
"diff-cover",
42+
"--compare-branch",
43+
{ type = "env", name = "DIFF_AGAINST", default = "origin/main" },
44+
"{work_dir}{/}coverage.{env_name}.xml",
45+
],
46+
]
47+
48+
[env.fix]
49+
description = "format the code base to adhere to our styles, and complain about what we cannot do automatically"
50+
skip_install = true
51+
deps = ["pre-commit-uv>=4.1.3"]
52+
pass_env = [{ type = "ref", of = ["env_run_base", "pass_env"] }, "PROGRAMDATA"]
53+
commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure", { type = "posargs" }]]
54+
55+
[env.type]
56+
description = "run type check on code base"
57+
deps = ["mypy==1.11.2", "types-cachetools>=5.5.0.20240820", "types-chardet>=5.0.4.6"]
58+
commands = [["mypy", "src/tox"], ["mypy", "tests"]]
59+
60+
[env.docs]
61+
description = "build documentation"
62+
extras = ["docs"]
63+
commands = [
64+
{ type = "posargs", default = [
65+
"sphinx-build",
66+
"-d",
67+
"{env_tmp_dir}{/}docs_tree",
68+
"docs",
69+
"{work_dir}{/}docs_out",
70+
"--color",
71+
"-b",
72+
"linkcheck",
73+
] },
74+
[
75+
"sphinx-build",
76+
"-d",
77+
"{env_tmp_dir}{/}docs_tree",
78+
"docs",
79+
"{work_dir}{/}docs_out",
80+
"--color",
81+
"-b",
82+
"html",
83+
"-W",
84+
],
85+
[
86+
"python",
87+
"-c",
88+
'print(r"documentation available under file://{work_dir}{/}docs_out{/}index.html")',
89+
],
90+
]
91+
92+
93+
[env.pkg_meta]
94+
description = "check that the long description is valid"
95+
skip_install = true
96+
deps = ["check-wheel-contents>=0.6", "twine>=5.1.1", "uv>=0.4.17"]
97+
commands = [
98+
[
99+
"uv",
100+
"build",
101+
"--sdist",
102+
"--wheel",
103+
"--out-dir",
104+
"{env_tmp_dir}",
105+
".",
106+
],
107+
[
108+
"twine",
109+
"check",
110+
"{env_tmp_dir}{/}*",
111+
],
112+
[
113+
"check-wheel-contents",
114+
"--no-config",
115+
"{env_tmp_dir}",
116+
],
117+
]
118+
119+
[env.release]
120+
description = "do a release, required posargs of the version number"
121+
skip_install = true
122+
deps = ["gitpython>=3.1.43", "packaging>=24.1", "towncrier>=24.8"]
123+
commands = [["python", "{tox_root}/tasks/release.py", "--version", "{posargs}"]]
124+
125+
[env.dev]
126+
description = "dev environment with all deps at {envdir}"
127+
package = "editable"
128+
deps = { type = "ref", of = ["env", "release", "deps"] }
129+
extras = ["docs", "testing"]
130+
commands = [["python", "-m", "pip", "list", "--format=columns"], ["python", "-c", 'print(r"{env_python}")']]
131+
uv_seed = true

0 commit comments

Comments
 (0)