Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/2567.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fallback to ``editable-legacy`` if package target is ``editable`` but the build backend does not have ``build_editable``
hook - by :user:`gaborbernat`.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies = [
"packaging>=21.3",
"platformdirs>=2.5.4",
"pluggy>=1",
"pyproject-api>=1.1.2",
"pyproject-api>=1.2.1",
'tomli>=2.0.1; python_version < "3.11"',
"virtualenv>=20.17",
'importlib-metadata>=5.1; python_version < "3.8"',
Expand All @@ -46,7 +46,7 @@ optional-dependencies.docs = [
optional-dependencies.testing = [
"build[virtualenv]>=0.9",
"covdefaults>=2.2.2",
"devpi-process>=0.2",
"devpi-process>=0.3",
"diff-cover>=7.2",
"distlib>=0.3.6",
"filelock>=3.8",
Expand Down
3 changes: 2 additions & 1 deletion src/tox/tox_env/python/virtual_env/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ def virtualenv_env_vars(self) -> dict[str, str]:
env["VIRTUALENV_COPIES"] = str(getattr(self.options, "always_copy", False) or self.conf["always_copy"])
env["VIRTUALENV_DOWNLOAD"] = str(self.conf["download"])
env["VIRTUALENV_PYTHON"] = "\n".join(base_python)
env["VIRTUALENV_TRY_FIRST_WITH"] = os.pathsep.join(self.options.discover)
if hasattr(self.options, "discover"):
env["VIRTUALENV_TRY_FIRST_WITH"] = os.pathsep.join(self.options.discover)
return env

@property
Expand Down
28 changes: 20 additions & 8 deletions src/tox/tox_env/python/virtual_env/package/pyproject.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import logging
import os
import sys
from contextlib import contextmanager
Expand Down Expand Up @@ -61,6 +62,10 @@ def __init__(self, backend_failed: BackendFailed) -> None:
)


class BuildEditableNotSupported(RuntimeError):
"""raised when build editable is not supported"""


class ToxCmdStatus(CmdStatus):
def __init__(self, execute_status: ExecuteStatus) -> None:
self._execute_status = execute_status
Expand Down Expand Up @@ -136,6 +141,8 @@ def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], Pac
def _setup_env(self) -> None:
super()._setup_env()
if "editable" in self.builds:
if not self._frontend.optional_hooks["build_editable"]:
raise BuildEditableNotSupported
build_requires = self._frontend.get_requires_for_build_editable().requires
self.installer.install(build_requires, PythonPackageToxEnv.__name__, "requires_for_build_editable")
if "wheel" in self.builds:
Expand All @@ -159,7 +166,17 @@ def _teardown(self) -> None:

def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
"""build the package to install"""
deps = self._load_deps(for_env)
try:
deps = self._load_deps(for_env)
except BuildEditableNotSupported:
logging.error(
f"package config for {for_env.env_name} is editable, however the build backend {self._frontend.backend}"
f" does not support PEP-660, falling back to editable-legacy - change your configuration to it",
)
self.builds.remove("editable")
self.builds.add("editable-legacy")
for_env._defined["package"].value = "editable-legacy" # type: ignore
deps = self._load_deps(for_env)
of_type: str = for_env["package"]
if of_type == "editable-legacy":
self.setup()
Expand All @@ -176,8 +193,8 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
return w_env.perform_packaging(for_env)
else:
self.setup()
method = "build_editable" if of_type == "editable" else "build_wheel"
with self._pkg_lock:
method = "build_editable" if of_type == "editable" else "build_wheel"
path = getattr(self._frontend, method)(
wheel_directory=self.pkg_dir,
metadata_directory=self.meta_folder,
Expand Down Expand Up @@ -293,12 +310,7 @@ def _send(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]:
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
if "wheel" in self._tox_env.builds or "editable" in self._tox_env.builds:
result = {
"code": 1,
"exc_type": "AvoidRedundant",
"exc_msg": "will need to build wheel either way, avoid prepare",
}
raise BackendFailed(result, "", "")
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
Expand Down
2 changes: 2 additions & 0 deletions tests/session/cmd/test_sequential.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def test_result_json_sequential(

assert packaging_setup == [
(0, "install_requires"),
(None, "_optional_hooks"),
(None, "get_requires_for_build_wheel"),
(0, "install_requires_for_build_wheel"),
(0, "freeze"),
Expand Down Expand Up @@ -284,6 +285,7 @@ def test_skip_develop_mode(tox_project: ToxProjectCreator, demo_pkg_setuptools:
calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
expected = [
(".pkg", "install_requires"),
(".pkg", "_optional_hooks"),
(".pkg", "get_requires_for_build_editable"),
(".pkg", "install_requires_for_build_editable"),
(".pkg", "build_editable"),
Expand Down
14 changes: 11 additions & 3 deletions tests/tox_env/python/pip/test_pip_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def test_pkg_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline
result_first.assert_success()
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
assert run_ids == [
"_optional_hooks",
"get_requires_for_build_wheel",
"build_wheel",
"install_package_deps",
Expand All @@ -156,7 +157,7 @@ def test_pkg_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline
result_second.assert_success()
assert "py: recreate env because dependencies removed: wheel" in result_second.out, result_second.out
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
assert run_ids == ["get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
assert run_ids == ["_optional_hooks", "get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]


def test_pkg_env_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
Expand All @@ -172,15 +173,22 @@ def test_pkg_env_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_in
result_first = proj.run("r")
result_first.assert_success()
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
assert run_ids == ["install_requires", "get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
assert run_ids == [
"install_requires",
"_optional_hooks",
"get_requires_for_build_wheel",
"build_wheel",
"install_package",
"_exit",
]
execute_calls.reset_mock()

(proj.path / "pyproject.toml").write_text(toml)
result_second = proj.run("r")
result_second.assert_success()
assert ".pkg: recreate env because dependencies removed: setuptools" in result_second.out, result_second.out
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
assert run_ids == ["get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
assert run_ids == ["_optional_hooks", "get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]


def test_pip_install_requirements_file_deps(tox_project: ToxProjectCreator) -> None:
Expand Down
1 change: 1 addition & 0 deletions tests/tox_env/python/test_python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_build_wheel_in_non_base_pkg_env(
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
assert calls == [
(f".pkg-{impl}{prev_ver}", "_optional_hooks"),
(f".pkg-{impl}{prev_ver}", "get_requires_for_build_wheel"),
(f".pkg-{impl}{prev_ver}", "build_wheel"),
(f"py{prev_ver}", "install_package"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_tox_install_pkg_sdist(tox_project: ToxProjectCreator, pkg_with_extras_p
deps = ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
assert calls == [
(".pkg_external_sdist_meta", "install_requires", ["setuptools", "wheel"]),
(".pkg_external_sdist_meta", "_optional_hooks", []),
(".pkg_external_sdist_meta", "get_requires_for_build_sdist", []),
(".pkg_external_sdist_meta", "prepare_metadata_for_build_wheel", []),
("py", "install_package_deps", deps),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_pyproject_deps_from_static(
result = proj.run("r", "--notest")
result.assert_success()

expected_calls = [(".pkg", "get_requires_for_build_sdist"), (".pkg", "build_sdist")]
expected_calls = [(".pkg", "_optional_hooks"), (".pkg", "get_requires_for_build_sdist"), (".pkg", "build_sdist")]
if deps:
expected_calls.append(("py", "install_package_deps"))
expected_calls.extend((("py", "install_package"), (".pkg", "_exit")))
Expand Down Expand Up @@ -157,8 +157,8 @@ def test_pyproject_deps_static_with_dynamic(
result.assert_success()

expected_calls = [
(".pkg", "_optional_hooks"),
(".pkg", "get_requires_for_build_sdist"),
(".pkg", "prepare_metadata_for_build_wheel"),
(".pkg", "build_wheel"),
(".pkg", "build_sdist"),
("py", "install_package_deps"),
Expand All @@ -170,3 +170,25 @@ def test_pyproject_deps_static_with_dynamic(

args = execute_calls.call_args_list[-3][0][3].cmd
assert args == ["python", "-I", "-m", "pip", "install", *deps]


def test_pyproject_no_build_editable_fallback(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
proj = tox_project({"tox.ini": ""}, base=demo_pkg_inline)
execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
result = proj.run("r", "--notest", "--develop")
result.assert_success()
warning = (
".pkg: package config for py is editable, however the build backend build does not support PEP-660, "
"falling back to editable-legacy - change your configuration to it"
)
assert warning in result.out.splitlines()

expected_calls = [
(".pkg", "_optional_hooks"),
(".pkg", "build_wheel"),
(".pkg", "get_requires_for_build_sdist"),
("py", "install_package"),
(".pkg", "_exit"),
]
found_calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
assert found_calls == expected_calls
4 changes: 2 additions & 2 deletions tests/tox_env/python/virtual_env/test_setuptools.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ def test_setuptools_package(
assert len(py_messages) == 5, "\n".join(py_messages) # 1 install wheel + 3 command + 1 final report

package_messages = [i for i in result if ".pkg: " in i]
# 1 install requires + 1 build requires + 1 build meta + 1 build isolated + 1 exit
assert len(package_messages) == 5, "\n".join(package_messages)
# 1 optional hooks + 1 install requires + 1 build requires + 1 build meta + 1 build isolated + 1 exit
assert len(package_messages) == 6, "\n".join(package_messages)