Skip to content

Commit ec7c0aa

Browse files
nschloegaborbernat
andauthored
Add config_settings support for build backend calls (#3090)
Co-authored-by: Bernát Gábor <[email protected]>
1 parent ce3c96e commit ec7c0aa

File tree

7 files changed

+267
-21
lines changed

7 files changed

+267
-21
lines changed

docs/changelog/3090.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for setting build backend ``config_settings`` in the configuration file - by :user:`gaborbernat`.

docs/config.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,54 @@ Python virtual environment packaging
742742

743743
Directory where to put project packages.
744744

745+
.. conf::
746+
:keys: config_settings_get_requires_for_build_sdist
747+
:version_added: 4.11
748+
749+
Config settings (``dict[str, str]``) passed to the ``get_requires_for_build_sdist`` backend API endpoint.
750+
751+
.. conf::
752+
:keys: config_settings_build_sdist
753+
:version_added: 4.11
754+
755+
Config settings (``dict[str, str]``) passed to the ``build_sdist`` backend API endpoint.
756+
757+
.. conf::
758+
:keys: config_settings_get_requires_for_build_wheel
759+
:version_added: 4.11
760+
761+
Config settings (``dict[str, str]``) passed to the ``get_requires_for_build_wheel`` backend API endpoint.
762+
763+
.. conf::
764+
:keys: config_settings_prepare_metadata_for_build_wheel
765+
:version_added: 4.11
766+
767+
Config settings (``dict[str, str]``) passed to the ``prepare_metadata_for_build_wheel`` backend API endpoint.
768+
769+
.. conf::
770+
:keys: config_settings_build_wheel
771+
:version_added: 4.11
772+
773+
Config settings (``dict[str, str]``) passed to the ``build_wheel`` backend API endpoint.
774+
775+
.. conf::
776+
:keys: config_settings_get_requires_for_build_editable
777+
:version_added: 4.11
778+
779+
Config settings (``dict[str, str]``) passed to the ``get_requires_for_build_editable`` backend API endpoint.
780+
781+
.. conf::
782+
:keys: config_settings_prepare_metadata_for_build_editable
783+
:version_added: 4.11
784+
785+
Config settings (``dict[str, str]``) passed to the ``prepare_metadata_for_build_editable`` backend API endpoint.
786+
787+
.. conf::
788+
:keys: config_settings_build_editable
789+
:version_added: 4.11
790+
791+
Config settings (``dict[str, str]``) passed to the ``build_editable`` backend API endpoint.
792+
745793
Pip installer
746794
~~~~~~~~~~~~~
747795

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ dependencies = [
5050
"cachetools>=5.3.1",
5151
"chardet>=5.2",
5252
"colorama>=0.4.6",
53-
"filelock>=3.12.2",
53+
"filelock>=3.12.3",
5454
'importlib-metadata>=6.8; python_version < "3.8"',
5555
"packaging>=23.1",
5656
"platformdirs>=3.10",
5757
"pluggy>=1.3",
58-
"pyproject-api>=1.5.4",
58+
"pyproject-api>=1.6.1",
5959
'tomli>=2.0.1; python_version < "3.11"',
6060
'typing-extensions>=4.7.1; python_version < "3.8"',
6161
"virtualenv>=20.24.3",

src/tox/tox_env/python/virtual_env/package/pyproject.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@
88
from itertools import chain
99
from pathlib import Path
1010
from threading import RLock
11-
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, NoReturn, Optional, Sequence, cast
11+
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, Literal, NoReturn, Optional, Sequence, cast
1212

1313
from cachetools import cached
1414
from packaging.requirements import Requirement
15-
from pyproject_api import BackendFailed, CmdStatus, Frontend
15+
from pyproject_api import (
16+
BackendFailed,
17+
CmdStatus,
18+
Frontend,
19+
MetadataForBuildEditableResult,
20+
MetadataForBuildWheelResult,
21+
)
1622

1723
from tox.execute.pep517_backend import LocalSubProcessPep517Executor
1824
from tox.execute.request import StdinSource
@@ -127,6 +133,23 @@ def register_config(self) -> None:
127133
default=lambda conf, name: self.env_dir / "dist", # noqa: ARG005
128134
desc="directory where to put project packages",
129135
)
136+
for key in ("sdist", "wheel", "editable"):
137+
self._add_config_settings(key)
138+
139+
def _add_config_settings(self, build_type: str) -> None:
140+
# config settings passed to PEP-517-compliant build backend https://peps.python.org/pep-0517/#config-settings
141+
keys = {
142+
"sdist": ["get_requires_for_build_sdist", "build_sdist"],
143+
"wheel": ["get_requires_for_build_wheel", "prepare_metadata_for_build_wheel", "build_wheel"],
144+
"editable": ["get_requires_for_build_editable", "prepare_metadata_for_build_editable", "build_editable"],
145+
}
146+
for key in keys.get(build_type, []):
147+
self.conf.add_config(
148+
keys=[f"config_settings_{key}"],
149+
of_type=Dict[str, str],
150+
default=None, # type: ignore[arg-type]
151+
desc=f"config settings passed to the {key} backend API endpoint",
152+
)
130153

131154
@property
132155
def pkg_dir(self) -> Path:
@@ -164,7 +187,8 @@ def _setup_env(self) -> None:
164187
self._setup_build_requires("editable")
165188

166189
def _setup_build_requires(self, of_type: str) -> None:
167-
requires = getattr(self._frontend, f"get_requires_for_build_{of_type}")().requires
190+
settings: ConfigSettings = self.conf[f"config_settings_get_requires_for_build_{of_type}"]
191+
requires = getattr(self._frontend, f"get_requires_for_build_{of_type}")(config_settings=settings).requires
168192
self._install(requires, PythonPackageToxEnv.__name__, f"requires_for_build_{of_type}")
169193

170194
def _teardown(self) -> None:
@@ -206,12 +230,15 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
206230
of_type: str = for_env["package"]
207231
if of_type == "editable-legacy":
208232
self.setup()
209-
deps = [*self.requires(), *self._frontend.get_requires_for_build_sdist().requires, *deps]
233+
config_settings: ConfigSettings = self.conf["config_settings_get_requires_for_build_sdist"]
234+
sdist_requires = self._frontend.get_requires_for_build_sdist(config_settings=config_settings).requires
235+
deps = [*self.requires(), *sdist_requires, *deps]
210236
package: Package = EditableLegacyPackage(self.core["tox_root"], deps) # the folder itself is the package
211237
elif of_type == "sdist":
212238
self.setup()
213239
with self._pkg_lock:
214-
sdist = self._frontend.build_sdist(sdist_directory=self.pkg_dir).sdist
240+
config_settings = self.conf["config_settings_build_sdist"]
241+
sdist = self._frontend.build_sdist(sdist_directory=self.pkg_dir, config_settings=config_settings).sdist
215242
sdist = create_session_view(sdist, self._package_temp_path)
216243
self._package_paths.add(sdist)
217244
package = SdistPackage(sdist, deps)
@@ -223,11 +250,12 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
223250
else:
224251
self.setup()
225252
method = "build_editable" if of_type == "editable" else "build_wheel"
253+
config_settings = self.conf[f"config_settings_{method}"]
226254
with self._pkg_lock:
227255
wheel = getattr(self._frontend, method)(
228256
wheel_directory=self.pkg_dir,
229257
metadata_directory=self.meta_folder_if_populated,
230-
config_settings=self._wheel_config_settings,
258+
config_settings=config_settings,
231259
).wheel
232260
wheel = create_session_view(wheel, self._package_temp_path)
233261
self._package_paths.add(wheel)
@@ -313,17 +341,20 @@ def _ensure_meta_present(self, for_env: EnvConfigSet) -> None:
313341
if self._distribution_meta is not None: # pragma: no branch
314342
return # pragma: no cover
315343
# even if we don't build a wheel we need the requirements for it should we want to build its metadata
316-
target = "editable" if for_env["package"] == "editable" else "wheel"
344+
target: Literal["editable", "wheel"] = "editable" if for_env["package"] == "editable" else "wheel"
317345
self.call_require_hooks.add(target)
318346

319347
self.setup()
320348
hook = getattr(self._frontend, f"prepare_metadata_for_build_{target}")
321-
dist_info = hook(self.meta_folder, self._wheel_config_settings).metadata
322-
self._distribution_meta = Distribution.at(str(dist_info))
323-
324-
@property
325-
def _wheel_config_settings(self) -> ConfigSettings | None:
326-
return {"--build-option": []}
349+
config: ConfigSettings = self.conf[f"config_settings_prepare_metadata_for_build_{target}"]
350+
result: MetadataForBuildWheelResult | MetadataForBuildEditableResult | None = hook(self.meta_folder, config)
351+
if result is None:
352+
config = self.conf[f"config_settings_build_{target}"]
353+
dist_info_path, _, __ = self._frontend.metadata_from_built(self.meta_folder, target, config)
354+
dist_info = str(dist_info_path)
355+
else:
356+
dist_info = str(result.metadata)
357+
self._distribution_meta = Distribution.at(dist_info)
327358

328359
def requires(self) -> tuple[Requirement, ...]:
329360
return self._frontend.requires
@@ -353,16 +384,18 @@ def backend_cmd(self) -> Sequence[str]:
353384

354385
def _send(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]:
355386
try:
356-
if (
357-
cmd in ("prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable")
358-
# given we'll build a wheel we might skip the prepare step
359-
and ("wheel" in self._tox_env.builds or "editable" in self._tox_env.builds)
360-
):
387+
if self._can_skip_prepare(cmd):
361388
return None, "", "" # will need to build wheel either way, avoid prepare
362389
return super()._send(cmd, **kwargs)
363390
except BackendFailed as exception:
364391
raise exception if isinstance(exception, ToxBackendFailed) else ToxBackendFailed(exception) from exception
365392

393+
def _can_skip_prepare(self, cmd: str) -> bool:
394+
# given we'll build a wheel we might skip the prepare step
395+
return cmd in ("prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable") and (
396+
"wheel" in self._tox_env.builds or "editable" in self._tox_env.builds
397+
)
398+
366399
@contextmanager
367400
def _send_msg(
368401
self,

tests/demo_pkg_inline/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def build_wheel(
9898
str(Path(sub_directory) / filename),
9999
)
100100
else:
101-
for arc_name, data in metadata_files.items(): # pragma: no branch
101+
for arc_name, data in metadata_files.items():
102102
zip_file_handler.writestr(arc_name, dedent(data).strip())
103103
print(f"created wheel {path}") # noqa: T201
104104
return base_name

tests/tox_env/python/virtual_env/package/test_package_pyproject.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
from __future__ import annotations
22

3+
import json
34
from textwrap import dedent
45
from typing import TYPE_CHECKING
56

67
import pytest
78

9+
from tox.execute.local_sub_process import LocalSubprocessExecuteStatus
10+
from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvFrontend
11+
812
if TYPE_CHECKING:
913
from pathlib import Path
1014

15+
from pytest_mock import MockerFixture
16+
1117
from tox.pytest import ToxProjectCreator
1218

1319

@@ -295,3 +301,160 @@ def test_pyproject_build_editable_and_wheel(tox_project: ToxProjectCreator, demo
295301
("d", "install_package"),
296302
(".pkg", "_exit"),
297303
]
304+
305+
306+
def test_pyproject_config_settings_sdist(
307+
tox_project: ToxProjectCreator,
308+
demo_pkg_setuptools: Path,
309+
mocker: MockerFixture,
310+
) -> None:
311+
ini = """
312+
[tox]
313+
env_list = sdist
314+
315+
[testenv]
316+
wheel_build_env = .pkg
317+
package = sdist
318+
319+
[testenv:.pkg]
320+
config_settings_get_requires_for_build_sdist = A = 1
321+
config_settings_build_sdist = B = 2
322+
config_settings_get_requires_for_build_wheel = C = 3
323+
config_settings_prepare_metadata_for_build_wheel = D = 4
324+
"""
325+
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
326+
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
327+
328+
write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
329+
330+
result = proj.run("r", "--notest", from_cwd=proj.path)
331+
result.assert_success()
332+
333+
found = {
334+
message["cmd"]: message["kwargs"]["config_settings"]
335+
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
336+
if not message["cmd"].startswith("_")
337+
}
338+
assert found == {
339+
"build_sdist": {"B": "2"},
340+
"get_requires_for_build_sdist": {"A": "1"},
341+
"get_requires_for_build_wheel": {"C": "3"},
342+
"prepare_metadata_for_build_wheel": {"D": "4"},
343+
}
344+
345+
346+
def test_pyproject_config_settings_wheel(
347+
tox_project: ToxProjectCreator,
348+
demo_pkg_setuptools: Path,
349+
mocker: MockerFixture,
350+
) -> None:
351+
ini = """
352+
[tox]
353+
env_list = wheel
354+
355+
[testenv]
356+
wheel_build_env = .pkg
357+
package = wheel
358+
359+
[testenv:.pkg]
360+
config_settings_get_requires_for_build_wheel = C = 3
361+
config_settings_prepare_metadata_for_build_wheel = D = 4
362+
config_settings_build_wheel = E = 5
363+
"""
364+
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
365+
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
366+
367+
write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
368+
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)
369+
370+
result = proj.run("r", "--notest", from_cwd=proj.path)
371+
result.assert_success()
372+
373+
found = {
374+
message["cmd"]: message["kwargs"]["config_settings"]
375+
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
376+
if not message["cmd"].startswith("_")
377+
}
378+
assert found == {
379+
"get_requires_for_build_wheel": {"C": "3"},
380+
"prepare_metadata_for_build_wheel": {"D": "4"},
381+
"build_wheel": {"E": "5"},
382+
}
383+
384+
385+
def test_pyproject_config_settings_editable(
386+
tox_project: ToxProjectCreator,
387+
demo_pkg_setuptools: Path,
388+
mocker: MockerFixture,
389+
) -> None:
390+
ini = """
391+
[tox]
392+
env_list = editable
393+
394+
[testenv:.pkg]
395+
config_settings_get_requires_for_build_editable = F = 6
396+
config_settings_prepare_metadata_for_build_editable = G = 7
397+
config_settings_build_editable = H = 8
398+
399+
[testenv]
400+
wheel_build_env = .pkg
401+
package = editable
402+
"""
403+
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
404+
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
405+
406+
write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
407+
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)
408+
409+
result = proj.run("r", "--notest", from_cwd=proj.path)
410+
result.assert_success()
411+
412+
found = {
413+
message["cmd"]: message["kwargs"]["config_settings"]
414+
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
415+
if not message["cmd"].startswith("_")
416+
}
417+
assert found == {
418+
"get_requires_for_build_editable": {"F": "6"},
419+
"prepare_metadata_for_build_editable": {"G": "7"},
420+
"build_editable": {"H": "8"},
421+
}
422+
423+
424+
def test_pyproject_config_settings_editable_legacy(
425+
tox_project: ToxProjectCreator,
426+
demo_pkg_setuptools: Path,
427+
mocker: MockerFixture,
428+
) -> None:
429+
ini = """
430+
[tox]
431+
env_list = editable
432+
433+
[testenv:.pkg]
434+
config_settings_get_requires_for_build_sdist = A = 1
435+
config_settings_get_requires_for_build_wheel = C = 3
436+
config_settings_prepare_metadata_for_build_wheel = D = 4
437+
438+
[testenv]
439+
wheel_build_env = .pkg
440+
package = editable-legacy
441+
"""
442+
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
443+
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
444+
445+
write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
446+
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)
447+
448+
result = proj.run("r", "--notest", from_cwd=proj.path)
449+
result.assert_success()
450+
451+
found = {
452+
message["cmd"]: message["kwargs"]["config_settings"]
453+
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
454+
if not message["cmd"].startswith("_")
455+
}
456+
assert found == {
457+
"get_requires_for_build_sdist": {"A": "1"},
458+
"get_requires_for_build_wheel": {"C": "3"},
459+
"prepare_metadata_for_build_wheel": {"D": "4"},
460+
}

0 commit comments

Comments
 (0)