Skip to content

Commit

Permalink
Add config_settings support for build backends
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Aug 29, 2023
1 parent ce3c96e commit f95523c
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 19 deletions.
1 change: 1 addition & 0 deletions docs/changelog/3090.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for setting build backend ``config_settings`` in the configuration file - by :user:`gaborbernat`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ dependencies = [
"cachetools>=5.3.1",
"chardet>=5.2",
"colorama>=0.4.6",
"filelock>=3.12.2",
"filelock>=3.12.3",
'importlib-metadata>=6.8; python_version < "3.8"',
"packaging>=23.1",
"platformdirs>=3.10",
Expand Down
71 changes: 53 additions & 18 deletions src/tox/tox_env/python/virtual_env/package/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
from itertools import chain
from pathlib import Path
from threading import RLock
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, NoReturn, Optional, Sequence, cast
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, Literal, NoReturn, Optional, Sequence, cast

from cachetools import cached
from packaging.requirements import Requirement
from pyproject_api import BackendFailed, CmdStatus, Frontend
from pyproject_api import (
BackendFailed,
CmdStatus,
Frontend,
MetadataForBuildEditableResult,
MetadataForBuildWheelResult,
)

from tox.execute.pep517_backend import LocalSubProcessPep517Executor
from tox.execute.request import StdinSource
Expand Down Expand Up @@ -127,6 +133,26 @@ def register_config(self) -> None:
default=lambda conf, name: self.env_dir / "dist", # noqa: ARG005
desc="directory where to put project packages",
)
self._add_config_settings()

def _add_config_settings(self) -> None:
# config settings passed to PEP-517-compliant build backend https://peps.python.org/pep-0517/#config-settings
for key in (
"get_requires_for_build_sdist",
"build_sdist",
"get_requires_for_build_wheel",
"prepare_metadata_for_build_wheel",
"build_wheel",
"get_requires_for_build_editable",
"prepare_metadata_for_build_editable",
"build_editable",
):
self.conf.add_config(
keys=[f"config_settings_{key}"],
of_type=Dict[str, str],
default=None, # type: ignore[arg-type]
desc=f"config settings passed to the {key} backend API endpoint",
)

@property
def pkg_dir(self) -> Path:
Expand Down Expand Up @@ -164,7 +190,8 @@ def _setup_env(self) -> None:
self._setup_build_requires("editable")

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

def _teardown(self) -> None:
Expand Down Expand Up @@ -206,12 +233,15 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
of_type: str = for_env["package"]
if of_type == "editable-legacy":
self.setup()
deps = [*self.requires(), *self._frontend.get_requires_for_build_sdist().requires, *deps]
config_settings: ConfigSettings = self.conf["config_settings_get_requires_for_build_sdist"]
sdist_requires = self._frontend.get_requires_for_build_sdist(config_settings=config_settings).requires
deps = [*self.requires(), *sdist_requires, *deps]
package: Package = EditableLegacyPackage(self.core["tox_root"], deps) # the folder itself is the package
elif of_type == "sdist":
self.setup()
with self._pkg_lock:
sdist = self._frontend.build_sdist(sdist_directory=self.pkg_dir).sdist
config_settings = self.conf["config_settings_build_sdist"]
sdist = self._frontend.build_sdist(sdist_directory=self.pkg_dir, config_settings=config_settings).sdist
sdist = create_session_view(sdist, self._package_temp_path)
self._package_paths.add(sdist)
package = SdistPackage(sdist, deps)
Expand All @@ -223,11 +253,12 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
else:
self.setup()
method = "build_editable" if of_type == "editable" else "build_wheel"
config_settings = self.conf[f"config_settings_{method}"]
with self._pkg_lock:
wheel = getattr(self._frontend, method)(
wheel_directory=self.pkg_dir,
metadata_directory=self.meta_folder_if_populated,
config_settings=self._wheel_config_settings,
config_settings=config_settings,
).wheel
wheel = create_session_view(wheel, self._package_temp_path)
self._package_paths.add(wheel)
Expand Down Expand Up @@ -313,17 +344,19 @@ def _ensure_meta_present(self, for_env: EnvConfigSet) -> None:
if self._distribution_meta is not None: # pragma: no branch
return # pragma: no cover
# even if we don't build a wheel we need the requirements for it should we want to build its metadata
target = "editable" if for_env["package"] == "editable" else "wheel"
target: Literal["editable", "wheel"] = "editable" if for_env["package"] == "editable" else "wheel"
self.call_require_hooks.add(target)

self.setup()
hook = getattr(self._frontend, f"prepare_metadata_for_build_{target}")
dist_info = hook(self.meta_folder, self._wheel_config_settings).metadata
self._distribution_meta = Distribution.at(str(dist_info))

@property
def _wheel_config_settings(self) -> ConfigSettings | None:
return {"--build-option": []}
config: ConfigSettings = self.conf[f"config_settings_prepare_metadata_for_build_{target}"]
result: MetadataForBuildWheelResult | MetadataForBuildEditableResult | None = hook(self.meta_folder, config)
if result is None:
config = self.conf[f"config_settings_build_{target}"]
dist_info_path, _, __ = self._frontend.metadata_from_built_wheel(target, self.meta_folder, config)
else:
dist_info_path = str(result.metadata)
self._distribution_meta = Distribution.at(dist_info_path)

def requires(self) -> tuple[Requirement, ...]:
return self._frontend.requires
Expand Down Expand Up @@ -353,16 +386,18 @@ def backend_cmd(self) -> Sequence[str]:

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

def _can_skip_prepare(self, cmd: str) -> bool:
# given we'll build a wheel we might skip the prepare step
return cmd in ("prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable") and (
"wheel" in self._tox_env.builds or "editable" in self._tox_env.builds
)

@contextmanager
def _send_msg(
self,
Expand Down
163 changes: 163 additions & 0 deletions tests/tox_env/python/virtual_env/package/test_package_pyproject.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from __future__ import annotations

import json
from textwrap import dedent
from typing import TYPE_CHECKING

import pytest

from tox.execute.local_sub_process import LocalSubprocessExecuteStatus
from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvFrontend

if TYPE_CHECKING:
from pathlib import Path

from pytest_mock import MockerFixture

from tox.pytest import ToxProjectCreator


Expand Down Expand Up @@ -295,3 +301,160 @@ def test_pyproject_build_editable_and_wheel(tox_project: ToxProjectCreator, demo
("d", "install_package"),
(".pkg", "_exit"),
]


def test_pyproject_config_settings_sdist(
tox_project: ToxProjectCreator,
demo_pkg_setuptools: Path,
mocker: MockerFixture,
) -> None:
ini = """
[tox]
env_list = sdist
[testenv]
wheel_build_env = .pkg
package = sdist
[testenv:.pkg]
config_settings_get_requires_for_build_sdist = A = 1
config_settings_build_sdist = B = 2
config_settings_get_requires_for_build_wheel = C = 3
config_settings_prepare_metadata_for_build_wheel = D = 4
"""
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)

write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")

result = proj.run("r", "--notest", from_cwd=proj.path)
result.assert_success()

found = {
message["cmd"]: message["kwargs"]["config_settings"]
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
if not message["cmd"].startswith("_")
}
assert found == {
"build_sdist": {"B": "2"},
"get_requires_for_build_sdist": {"A": "1"},
"get_requires_for_build_wheel": {"C": "3"},
"prepare_metadata_for_build_wheel": {"D": "4"},
}


def test_pyproject_config_settings_wheel(
tox_project: ToxProjectCreator,
demo_pkg_setuptools: Path,
mocker: MockerFixture,
) -> None:
ini = """
[tox]
env_list = wheel
[testenv]
wheel_build_env = .pkg
package = wheel
[testenv:.pkg]
config_settings_get_requires_for_build_wheel = C = 3
config_settings_prepare_metadata_for_build_wheel = D = 4
config_settings_build_wheel = E = 5
"""
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)

write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)

result = proj.run("r", "--notest", from_cwd=proj.path)
result.assert_success()

found = {
message["cmd"]: message["kwargs"]["config_settings"]
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
if not message["cmd"].startswith("_")
}
assert found == {
"get_requires_for_build_wheel": {"C": "3"},
"prepare_metadata_for_build_wheel": {"D": "4"},
"build_wheel": {"E": "5"},
}


def test_pyproject_config_settings_editable(
tox_project: ToxProjectCreator,
demo_pkg_setuptools: Path,
mocker: MockerFixture,
) -> None:
ini = """
[tox]
env_list = editable
[testenv:.pkg]
config_settings_get_requires_for_build_editable = F = 6
config_settings_prepare_metadata_for_build_editable = G = 7
config_settings_build_editable = H = 8
[testenv]
wheel_build_env = .pkg
package = editable
"""
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)

write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)

result = proj.run("r", "--notest", from_cwd=proj.path)
result.assert_success()

found = {
message["cmd"]: message["kwargs"]["config_settings"]
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
if not message["cmd"].startswith("_")
}
assert found == {
"get_requires_for_build_editable": {"F": "6"},
"prepare_metadata_for_build_editable": {"G": "7"},
"build_editable": {"H": "8"},
}


def test_pyproject_config_settings_editable_legacy(
tox_project: ToxProjectCreator,
demo_pkg_setuptools: Path,
mocker: MockerFixture,
) -> None:
ini = """
[tox]
env_list = editable
[testenv:.pkg]
config_settings_get_requires_for_build_sdist = A = 1
config_settings_get_requires_for_build_wheel = C = 3
config_settings_prepare_metadata_for_build_wheel = D = 4
[testenv]
wheel_build_env = .pkg
package = editable-legacy
"""
proj = tox_project({"tox.ini": ini}, base=demo_pkg_setuptools)
proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)

write_stdin = mocker.spy(LocalSubprocessExecuteStatus, "write_stdin")
mocker.patch.object(Pep517VirtualEnvFrontend, "_can_skip_prepare", return_value=False)

result = proj.run("r", "--notest", from_cwd=proj.path)
result.assert_success()

found = {
message["cmd"]: message["kwargs"]["config_settings"]
for message in [json.loads(call[0][1]) for call in write_stdin.call_args_list]
if not message["cmd"].startswith("_")
}
assert found == {
"get_requires_for_build_sdist": {"A": "1"},
"get_requires_for_build_wheel": {"C": "3"},
"prepare_metadata_for_build_wheel": {"D": "4"},
}

0 comments on commit f95523c

Please sign in to comment.