diff --git a/docs/changelog/2567.bugfix.rst b/docs/changelog/2567.bugfix.rst new file mode 100644 index 000000000..114aba2f0 --- /dev/null +++ b/docs/changelog/2567.bugfix.rst @@ -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`. diff --git a/pyproject.toml b/pyproject.toml index 1d0c21c5b..9505d7766 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"', @@ -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", diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py index bb75b61a4..01bdc627f 100644 --- a/src/tox/tox_env/python/virtual_env/api.py +++ b/src/tox/tox_env/python/virtual_env/api.py @@ -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 diff --git a/src/tox/tox_env/python/virtual_env/package/pyproject.py b/src/tox/tox_env/python/virtual_env/package/pyproject.py index f6b051ba6..5ee1d7641 100644 --- a/src/tox/tox_env/python/virtual_env/package/pyproject.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os import sys from contextlib import contextmanager @@ -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 @@ -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: @@ -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() @@ -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, @@ -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 diff --git a/tests/session/cmd/test_sequential.py b/tests/session/cmd/test_sequential.py index 2aa683a37..044e8bc5d 100644 --- a/tests/session/cmd/test_sequential.py +++ b/tests/session/cmd/test_sequential.py @@ -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"), @@ -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"), diff --git a/tests/tox_env/python/pip/test_pip_install.py b/tests/tox_env/python/pip/test_pip_install.py index 71dccd87b..bc4705738 100644 --- a/tests/tox_env/python/pip/test_pip_install.py +++ b/tests/tox_env/python/pip/test_pip_install.py @@ -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", @@ -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: @@ -172,7 +173,14 @@ 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) @@ -180,7 +188,7 @@ def test_pkg_env_dep_remove_recreate(tox_project: ToxProjectCreator, demo_pkg_in 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: diff --git a/tests/tox_env/python/test_python_api.py b/tests/tox_env/python/test_python_api.py index ebdf044d6..c2ae4a8f7 100644 --- a/tests/tox_env/python/test_python_api.py +++ b/tests/tox_env/python/test_python_api.py @@ -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"), diff --git a/tests/tox_env/python/virtual_env/package/test_package_cmd_builder.py b/tests/tox_env/python/virtual_env/package/test_package_cmd_builder.py index 98d942c0c..f3cc82972 100644 --- a/tests/tox_env/python/virtual_env/package/test_package_cmd_builder.py +++ b/tests/tox_env/python/virtual_env/package/test_package_cmd_builder.py @@ -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), diff --git a/tests/tox_env/python/virtual_env/package/test_package_pyproject.py b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py index 0e5154797..e8f21eb64 100644 --- a/tests/tox_env/python/virtual_env/package/test_package_pyproject.py +++ b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py @@ -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"))) @@ -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"), @@ -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 diff --git a/tests/tox_env/python/virtual_env/test_setuptools.py b/tests/tox_env/python/virtual_env/test_setuptools.py index 2e8ebc2b0..d96cc7fe2 100644 --- a/tests/tox_env/python/virtual_env/test_setuptools.py +++ b/tests/tox_env/python/virtual_env/test_setuptools.py @@ -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)