Skip to content

Commit 20ab8a0

Browse files
authored
Fallback to editable-legacy if the build backend does not have build_editable (#2590)
Resolves #2567
1 parent 279b247 commit 20ab8a0

File tree

10 files changed

+67
-18
lines changed

10 files changed

+67
-18
lines changed

docs/changelog/2567.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fallback to ``editable-legacy`` if package target is ``editable`` but the build backend does not have ``build_editable``
2+
hook - by :user:`gaborbernat`.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies = [
2727
"packaging>=21.3",
2828
"platformdirs>=2.5.4",
2929
"pluggy>=1",
30-
"pyproject-api>=1.1.2",
30+
"pyproject-api>=1.2.1",
3131
'tomli>=2.0.1; python_version < "3.11"',
3232
"virtualenv>=20.17",
3333
'importlib-metadata>=5.1; python_version < "3.8"',
@@ -46,7 +46,7 @@ optional-dependencies.docs = [
4646
optional-dependencies.testing = [
4747
"build[virtualenv]>=0.9",
4848
"covdefaults>=2.2.2",
49-
"devpi-process>=0.2",
49+
"devpi-process>=0.3",
5050
"diff-cover>=7.2",
5151
"distlib>=0.3.6",
5252
"filelock>=3.8",

src/tox/tox_env/python/virtual_env/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ def virtualenv_env_vars(self) -> dict[str, str]:
118118
env["VIRTUALENV_COPIES"] = str(getattr(self.options, "always_copy", False) or self.conf["always_copy"])
119119
env["VIRTUALENV_DOWNLOAD"] = str(self.conf["download"])
120120
env["VIRTUALENV_PYTHON"] = "\n".join(base_python)
121-
env["VIRTUALENV_TRY_FIRST_WITH"] = os.pathsep.join(self.options.discover)
121+
if hasattr(self.options, "discover"):
122+
env["VIRTUALENV_TRY_FIRST_WITH"] = os.pathsep.join(self.options.discover)
122123
return env
123124

124125
@property

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
import os
45
import sys
56
from contextlib import contextmanager
@@ -61,6 +62,10 @@ def __init__(self, backend_failed: BackendFailed) -> None:
6162
)
6263

6364

65+
class BuildEditableNotSupported(RuntimeError):
66+
"""raised when build editable is not supported"""
67+
68+
6469
class ToxCmdStatus(CmdStatus):
6570
def __init__(self, execute_status: ExecuteStatus) -> None:
6671
self._execute_status = execute_status
@@ -136,6 +141,8 @@ def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], Pac
136141
def _setup_env(self) -> None:
137142
super()._setup_env()
138143
if "editable" in self.builds:
144+
if not self._frontend.optional_hooks["build_editable"]:
145+
raise BuildEditableNotSupported
139146
build_requires = self._frontend.get_requires_for_build_editable().requires
140147
self.installer.install(build_requires, PythonPackageToxEnv.__name__, "requires_for_build_editable")
141148
if "wheel" in self.builds:
@@ -159,7 +166,17 @@ def _teardown(self) -> None:
159166

160167
def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
161168
"""build the package to install"""
162-
deps = self._load_deps(for_env)
169+
try:
170+
deps = self._load_deps(for_env)
171+
except BuildEditableNotSupported:
172+
logging.error(
173+
f"package config for {for_env.env_name} is editable, however the build backend {self._frontend.backend}"
174+
f" does not support PEP-660, falling back to editable-legacy - change your configuration to it",
175+
)
176+
self.builds.remove("editable")
177+
self.builds.add("editable-legacy")
178+
for_env._defined["package"].value = "editable-legacy" # type: ignore
179+
deps = self._load_deps(for_env)
163180
of_type: str = for_env["package"]
164181
if of_type == "editable-legacy":
165182
self.setup()
@@ -176,8 +193,8 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
176193
return w_env.perform_packaging(for_env)
177194
else:
178195
self.setup()
196+
method = "build_editable" if of_type == "editable" else "build_wheel"
179197
with self._pkg_lock:
180-
method = "build_editable" if of_type == "editable" else "build_wheel"
181198
path = getattr(self._frontend, method)(
182199
wheel_directory=self.pkg_dir,
183200
metadata_directory=self.meta_folder,
@@ -293,12 +310,7 @@ def _send(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]:
293310
if cmd in ("prepare_metadata_for_build_wheel", "prepare_metadata_for_build_editable"):
294311
# given we'll build a wheel we might skip the prepare step
295312
if "wheel" in self._tox_env.builds or "editable" in self._tox_env.builds:
296-
result = {
297-
"code": 1,
298-
"exc_type": "AvoidRedundant",
299-
"exc_msg": "will need to build wheel either way, avoid prepare",
300-
}
301-
raise BackendFailed(result, "", "")
313+
return None, "", "" # will need to build wheel either way, avoid prepare
302314
return super()._send(cmd, **kwargs)
303315
except BackendFailed as exception:
304316
raise exception if isinstance(exception, ToxBackendFailed) else ToxBackendFailed(exception) from exception

tests/session/cmd/test_sequential.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_result_json_sequential(
8383

8484
assert packaging_setup == [
8585
(0, "install_requires"),
86+
(None, "_optional_hooks"),
8687
(None, "get_requires_for_build_wheel"),
8788
(0, "install_requires_for_build_wheel"),
8889
(0, "freeze"),
@@ -284,6 +285,7 @@ def test_skip_develop_mode(tox_project: ToxProjectCreator, demo_pkg_setuptools:
284285
calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
285286
expected = [
286287
(".pkg", "install_requires"),
288+
(".pkg", "_optional_hooks"),
287289
(".pkg", "get_requires_for_build_editable"),
288290
(".pkg", "install_requires_for_build_editable"),
289291
(".pkg", "build_editable"),

tests/tox_env/python/pip/test_pip_install.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def test_pkg_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline
143143
result_first.assert_success()
144144
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
145145
assert run_ids == [
146+
"_optional_hooks",
146147
"get_requires_for_build_wheel",
147148
"build_wheel",
148149
"install_package_deps",
@@ -156,7 +157,7 @@ def test_pkg_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline
156157
result_second.assert_success()
157158
assert "py: recreate env because dependencies removed: wheel" in result_second.out, result_second.out
158159
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
159-
assert run_ids == ["get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
160+
assert run_ids == ["_optional_hooks", "get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
160161

161162

162163
def test_pkg_env_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
@@ -172,15 +173,22 @@ def test_pkg_env_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_in
172173
result_first = proj.run("r")
173174
result_first.assert_success()
174175
run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
175-
assert run_ids == ["install_requires", "get_requires_for_build_wheel", "build_wheel", "install_package", "_exit"]
176+
assert run_ids == [
177+
"install_requires",
178+
"_optional_hooks",
179+
"get_requires_for_build_wheel",
180+
"build_wheel",
181+
"install_package",
182+
"_exit",
183+
]
176184
execute_calls.reset_mock()
177185

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

185193

186194
def test_pip_install_requirements_file_deps(tox_project: ToxProjectCreator) -> None:

tests/tox_env/python/test_python_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def test_build_wheel_in_non_base_pkg_env(
5252
result.assert_success()
5353
calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
5454
assert calls == [
55+
(f".pkg-{impl}{prev_ver}", "_optional_hooks"),
5556
(f".pkg-{impl}{prev_ver}", "get_requires_for_build_wheel"),
5657
(f".pkg-{impl}{prev_ver}", "build_wheel"),
5758
(f"py{prev_ver}", "install_package"),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def test_tox_install_pkg_sdist(tox_project: ToxProjectCreator, pkg_with_extras_p
5252
deps = ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
5353
assert calls == [
5454
(".pkg_external_sdist_meta", "install_requires", ["setuptools", "wheel"]),
55+
(".pkg_external_sdist_meta", "_optional_hooks", []),
5556
(".pkg_external_sdist_meta", "get_requires_for_build_sdist", []),
5657
(".pkg_external_sdist_meta", "prepare_metadata_for_build_wheel", []),
5758
("py", "install_package_deps", deps),

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_pyproject_deps_from_static(
107107
result = proj.run("r", "--notest")
108108
result.assert_success()
109109

110-
expected_calls = [(".pkg", "get_requires_for_build_sdist"), (".pkg", "build_sdist")]
110+
expected_calls = [(".pkg", "_optional_hooks"), (".pkg", "get_requires_for_build_sdist"), (".pkg", "build_sdist")]
111111
if deps:
112112
expected_calls.append(("py", "install_package_deps"))
113113
expected_calls.extend((("py", "install_package"), (".pkg", "_exit")))
@@ -157,8 +157,8 @@ def test_pyproject_deps_static_with_dynamic(
157157
result.assert_success()
158158

159159
expected_calls = [
160+
(".pkg", "_optional_hooks"),
160161
(".pkg", "get_requires_for_build_sdist"),
161-
(".pkg", "prepare_metadata_for_build_wheel"),
162162
(".pkg", "build_wheel"),
163163
(".pkg", "build_sdist"),
164164
("py", "install_package_deps"),
@@ -170,3 +170,25 @@ def test_pyproject_deps_static_with_dynamic(
170170

171171
args = execute_calls.call_args_list[-3][0][3].cmd
172172
assert args == ["python", "-I", "-m", "pip", "install", *deps]
173+
174+
175+
def test_pyproject_no_build_editable_fallback(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
176+
proj = tox_project({"tox.ini": ""}, base=demo_pkg_inline)
177+
execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
178+
result = proj.run("r", "--notest", "--develop")
179+
result.assert_success()
180+
warning = (
181+
".pkg: package config for py is editable, however the build backend build does not support PEP-660, "
182+
"falling back to editable-legacy - change your configuration to it"
183+
)
184+
assert warning in result.out.splitlines()
185+
186+
expected_calls = [
187+
(".pkg", "_optional_hooks"),
188+
(".pkg", "build_wheel"),
189+
(".pkg", "get_requires_for_build_sdist"),
190+
("py", "install_package"),
191+
(".pkg", "_exit"),
192+
]
193+
found_calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
194+
assert found_calls == expected_calls

tests/tox_env/python/virtual_env/test_setuptools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ def test_setuptools_package(
4848
assert len(py_messages) == 5, "\n".join(py_messages) # 1 install wheel + 3 command + 1 final report
4949

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

0 commit comments

Comments
 (0)