diff --git a/.gitignore b/.gitignore index b5b2d5be1..0ae8ecde4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__ *.swp *.egg-info +/tests/demo_pkg_setuptools/build/lib/demo_pkg_setuptools/__init__.py diff --git a/docs/changelog/2502.feature.rst b/docs/changelog/2502.feature.rst new file mode 100644 index 000000000..1de68b294 --- /dev/null +++ b/docs/changelog/2502.feature.rst @@ -0,0 +1,2 @@ +Add support for editable wheels, make it the default development mode and rename ``dev-legacy`` mode to +``editable-legacy`` - by :user:`gaborbernat`. diff --git a/src/tox/tox_env/python/package.py b/src/tox/tox_env/python/package.py index ca8333395..129c3b609 100644 --- a/src/tox/tox_env/python/package.py +++ b/src/tox/tox_env/python/package.py @@ -39,8 +39,12 @@ class SdistPackage(PythonPathPackageWithDeps): """sdist package""" -class DevLegacyPackage(PythonPathPackageWithDeps): - """legacy dev package""" +class EditableLegacyPackage(PythonPathPackageWithDeps): + """legacy editable package""" + + +class EditablePackage(PythonPathPackageWithDeps): + """PEP-660 editable package""" class PythonPackageToxEnv(Python, PackageToxEnv, ABC): @@ -59,7 +63,11 @@ def requires(self) -> tuple[Requirement, ...] | PythonDeps: def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], PackageToxEnv, None]: yield from super().register_run_env(run_env) - if not isinstance(run_env, Python) or run_env.conf["package"] != "wheel" or "wheel_build_env" in run_env.conf: + if ( + not isinstance(run_env, Python) + or run_env.conf["package"] not in {"wheel", "editable"} + or "wheel_build_env" in run_env.conf + ): return def default_wheel_tag(conf: Config, env_name: str | None) -> str: # noqa: U100 diff --git a/src/tox/tox_env/python/pip/pip_install.py b/src/tox/tox_env/python/pip/pip_install.py index dbf1030b5..7bd7fe3ab 100644 --- a/src/tox/tox_env/python/pip/pip_install.py +++ b/src/tox/tox_env/python/pip/pip_install.py @@ -15,7 +15,7 @@ from tox.tox_env.installer import Installer from tox.tox_env.package import PathPackage from tox.tox_env.python.api import Python -from tox.tox_env.python.package import DevLegacyPackage, SdistPackage, WheelPackage +from tox.tox_env.python.package import EditableLegacyPackage, EditablePackage, SdistPackage, WheelPackage from tox.tox_env.python.pip.req_file import PythonDeps @@ -123,7 +123,9 @@ def _recreate_if_diff(of_type: str, new_opts: list[str], old_opts: list[str], fm def _install_list_of_deps( self, - arguments: Sequence[Requirement | WheelPackage | SdistPackage | DevLegacyPackage | PathPackage], + arguments: Sequence[ + Requirement | WheelPackage | SdistPackage | EditableLegacyPackage | EditablePackage | PathPackage + ], section: str, of_type: str, ) -> None: @@ -131,10 +133,10 @@ def _install_list_of_deps( for arg in arguments: if isinstance(arg, Requirement): groups["req"].append(str(arg)) - elif isinstance(arg, (WheelPackage, SdistPackage)): + elif isinstance(arg, (WheelPackage, SdistPackage, EditablePackage)): groups["req"].extend(str(i) for i in arg.deps) groups["pkg"].append(str(arg.path)) - elif isinstance(arg, DevLegacyPackage): + elif isinstance(arg, EditableLegacyPackage): groups["req"].extend(str(i) for i in arg.deps) groups["dev_pkg"].append(str(arg.path)) else: diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py index 61bcbf2da..591633d74 100644 --- a/src/tox/tox_env/python/runner.py +++ b/src/tox/tox_env/python/runner.py @@ -39,7 +39,7 @@ def register_config(self) -> None: @property def _package_types(self) -> tuple[str, ...]: - return "wheel", "sdist", "dev-legacy", "skip", "external" + return "wheel", "sdist", "editable", "editable-legacy", "skip", "external" def _register_package_conf(self) -> bool: # provision package type @@ -58,7 +58,7 @@ def _register_package_conf(self) -> bool: ) develop_mode = self.conf["use_develop"] or getattr(self.options, "develop", False) if develop_mode: - self.conf.add_constant(["package"], desc, "dev-legacy") + self.conf.add_constant(["package"], desc, "editable") else: self.conf.add_config(keys="package", of_type=str, default=self.default_pkg_type, desc=desc) diff --git a/src/tox/tox_env/python/virtual_env/package/cmd_builder.py b/src/tox/tox_env/python/virtual_env/package/cmd_builder.py index d43e2d1d8..6ec8d0359 100644 --- a/src/tox/tox_env/python/virtual_env/package/cmd_builder.py +++ b/src/tox/tox_env/python/virtual_env/package/cmd_builder.py @@ -116,7 +116,7 @@ def extract_install_info(self, for_env: EnvConfigSet, path: Path) -> list[Packag assert self._sdist_meta_tox_env is not None # the register run env is guaranteed to be called before this with self._sdist_meta_tox_env.display_context(self._has_display_suspended): self._sdist_meta_tox_env.root = next(work_dir.iterdir()) # contains a single egg info folder - deps = self._sdist_meta_tox_env.get_package_dependencies() + deps = self._sdist_meta_tox_env.get_package_dependencies(for_env) package = SdistPackage(path, dependencies_with_extras(deps, extras)) return [package] 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 e41eae44b..11e412f6a 100644 --- a/src/tox/tox_env/python/virtual_env/package/pyproject.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -19,7 +19,13 @@ from tox.tox_env.api import ToxEnvCreateArgs from tox.tox_env.errors import Fail from tox.tox_env.package import Package, PackageToxEnv -from tox.tox_env.python.package import DevLegacyPackage, PythonPackageToxEnv, SdistPackage, WheelPackage +from tox.tox_env.python.package import ( + EditableLegacyPackage, + EditablePackage, + PythonPackageToxEnv, + SdistPackage, + WheelPackage, +) from tox.tox_env.register import ToxEnvRegister from tox.tox_env.runner import RunToxEnv @@ -128,6 +134,9 @@ 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: + 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: build_requires = self._frontend.get_requires_for_build_wheel().requires self.installer.install(build_requires, PythonPackageToxEnv.__name__, "requires_for_build_wheel") @@ -151,15 +160,15 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: """build the package to install""" deps = self._load_deps(for_env) of_type: str = for_env["package"] - if of_type == "dev-legacy": + if of_type == "editable-legacy": self.setup() deps = [*self.requires(), *self._frontend.get_requires_for_build_sdist().requires] + deps - package: Package = DevLegacyPackage(self.core["tox_root"], deps) # the folder itself is the package + package: Package = EditableLegacyPackage(self.core["tox_root"], deps) # the folder itself is the package elif of_type == "sdist": self.setup() with self._pkg_lock: package = SdistPackage(self._frontend.build_sdist(sdist_directory=self.pkg_dir).sdist, deps) - elif of_type == "wheel": + elif of_type in {"wheel", "editable"}: w_env = self._wheel_build_envs.get(for_env["wheel_build_env"]) if w_env is not None and w_env is not self: with w_env.display_context(self._has_display_suspended): @@ -167,12 +176,13 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: else: self.setup() with self._pkg_lock: - path = self._frontend.build_wheel( + 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, config_settings=self._wheel_config_settings, ).wheel - package = WheelPackage(path, deps) + package = (EditablePackage if of_type == "editable" else WheelPackage)(path, deps) else: # pragma: no cover # for when we introduce new packaging types and don't implement raise TypeError(f"cannot handle package type {of_type}") # pragma: no cover return [package] @@ -209,38 +219,42 @@ def _load_deps_from_built_metadata(self, for_env: EnvConfigSet) -> list[Requirem # to calculate the package metadata, otherwise ourselves of_type: str = for_env["package"] reqs: list[Requirement] | None = None - if of_type == "wheel": # wheel packages + if of_type in ("wheel", "editable"): # wheel packages w_env = self._wheel_build_envs.get(for_env["wheel_build_env"]) if w_env is not None and w_env is not self: with w_env.display_context(self._has_display_suspended): - reqs = w_env.get_package_dependencies() if isinstance(w_env, Pep517VirtualEnvPackager) else [] + if isinstance(w_env, Pep517VirtualEnvPackager): + reqs = w_env.get_package_dependencies(for_env) + else: + reqs = [] if reqs is None: - reqs = self.get_package_dependencies() + reqs = self.get_package_dependencies(for_env) extras: set[str] = for_env["extras"] deps = dependencies_with_extras(reqs, extras) return deps - def get_package_dependencies(self) -> list[Requirement]: + def get_package_dependencies(self, for_env: EnvConfigSet) -> list[Requirement]: with self._pkg_lock: if self._package_dependencies is None: # pragma: no branch - self._ensure_meta_present() + self._ensure_meta_present(for_env) requires: list[str] = cast(PathDistribution, self._distribution_meta).requires or [] self._package_dependencies = [Requirement(i) for i in requires] # pragma: no branch return self._package_dependencies - def _ensure_meta_present(self) -> None: + def _ensure_meta_present(self, for_env: EnvConfigSet) -> None: if self._distribution_meta is not None: # pragma: no branch return # pragma: no cover self.setup() - dist_info = self._frontend.prepare_metadata_for_build_wheel( - self.meta_folder, - self._wheel_config_settings, - ).metadata + end = self._frontend + if for_env["package"] == "editable": + dist_info = end.prepare_metadata_for_build_editable(self.meta_folder, self._wheel_config_settings).metadata + else: + dist_info = end.prepare_metadata_for_build_wheel(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 {"--global-option": ["--bdist-dir", str(self.env_dir / "build")]} + return {"--build-option": []} def requires(self) -> tuple[Requirement, ...]: return self._frontend.requires @@ -258,6 +272,7 @@ def __init__(self, root: Path, env: Pep517VirtualEnvPackager) -> None: ) self.build_wheel = pkg_cache(self.build_wheel) # type: ignore self.build_sdist = pkg_cache(self.build_sdist) # type: ignore + self.build_editable = pkg_cache(self.build_editable) # type: ignore @property def backend_cmd(self) -> Sequence[str]: @@ -265,9 +280,9 @@ def backend_cmd(self) -> Sequence[str]: def _send(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]: try: - if cmd == "prepare_metadata_for_build_wheel": + 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: + if "wheel" in self._tox_env.builds or "editable" in self._tox_env.builds: result = { "code": 1, "exc_type": "AvoidRedundant", diff --git a/tests/demo_pkg_setuptools/pyproject.toml b/tests/demo_pkg_setuptools/pyproject.toml index 7fd4fb510..0f94e90bc 100644 --- a/tests/demo_pkg_setuptools/pyproject.toml +++ b/tests/demo_pkg_setuptools/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools>=45", "wheel>=0.33"] +requires = ["setuptools>=63"] build-backend = 'setuptools.build_meta' diff --git a/tests/session/cmd/test_sequential.py b/tests/session/cmd/test_sequential.py index 828abb967..66d6d7937 100644 --- a/tests/session/cmd/test_sequential.py +++ b/tests/session/cmd/test_sequential.py @@ -283,9 +283,9 @@ 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", "prepare_metadata_for_build_wheel"), - (".pkg", "get_requires_for_build_sdist"), - ("py", "install_package_deps"), + (".pkg", "get_requires_for_build_editable"), + (".pkg", "install_requires_for_build_editable"), + (".pkg", "build_editable"), ("py", "install_package"), (".pkg", "_exit"), ] diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py index ae8034224..0fa430291 100644 --- a/tests/session/cmd/test_show_config.py +++ b/tests/session/cmd/test_show_config.py @@ -197,7 +197,7 @@ def test_show_config_ini_comment_path(tox_project: ToxProjectCreator, tmp_path: def test_show_config_cli_flag(tox_project: ToxProjectCreator) -> None: project = tox_project({"tox.ini": "", "pyproject.toml": ""}) result = project.run("c", "-e", "py,.pkg", "-k", "package", "recreate", "--develop", "-r", "--no-recreate-pkg") - expected = "[testenv:py]\npackage = dev-legacy\nrecreate = True\n\n[testenv:.pkg]\nrecreate = False\n" + expected = "[testenv:py]\npackage = editable\nrecreate = True\n\n[testenv:.pkg]\nrecreate = False\n" assert result.out == expected 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 22ecaac54..0e5154797 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 @@ -9,7 +9,7 @@ @pytest.mark.parametrize( "pkg_type", - ["dev-legacy", "sdist", "wheel"], + ["editable-legacy", "editable", "sdist", "wheel"], ) def test_tox_ini_package_type_valid(tox_project: ToxProjectCreator, pkg_type: str) -> None: proj = tox_project({"tox.ini": f"[testenv]\npackage={pkg_type}", "pyproject.toml": ""}) @@ -25,11 +25,12 @@ def test_tox_ini_package_type_invalid(tox_project: ToxProjectCreator) -> None: proj = tox_project({"tox.ini": "[testenv]\npackage=bad", "pyproject.toml": ""}) result = proj.run("c", "-k", "package_tox_env_type") result.assert_failed() - assert " invalid package config type bad requested, must be one of wheel, sdist, dev-legacy, skip" in result.out + msg = " invalid package config type bad requested, must be one of wheel, sdist, editable, editable-legacy, skip" + assert msg in result.out def test_get_package_deps_different_extras(pkg_with_extras_project: Path, tox_project: ToxProjectCreator) -> None: - ini = "[testenv:a]\npackage=dev-legacy\nextras=docs\n[testenv:b]\npackage=sdist\nextras=format" + ini = "[testenv:a]\npackage=editable-legacy\nextras=docs\n[testenv:b]\npackage=sdist\nextras=format" proj = tox_project({"tox.ini": ini}) execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) result = proj.run("r", "--root", str(pkg_with_extras_project), "-e", "a,b")