Skip to content

Commit

Permalink
PEP-660 support
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <gaborjbernat@gmail.com>
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Nov 27, 2022
1 parent f7f5d68 commit 6b36a88
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
__pycache__
*.swp
*.egg-info
/tests/demo_pkg_setuptools/build/lib/demo_pkg_setuptools/__init__.py
2 changes: 2 additions & 0 deletions docs/changelog/2502.feature.rst
Original file line number Diff line number Diff line change
@@ -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`.
14 changes: 11 additions & 3 deletions src/tox/tox_env/python/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions src/tox/tox_env/python/pip/pip_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -123,18 +123,20 @@ 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:
groups: dict[str, list[str]] = defaultdict(list)
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:
Expand Down
4 changes: 2 additions & 2 deletions src/tox/tox_env/python/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/tox/tox_env/python/virtual_env/package/cmd_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
53 changes: 34 additions & 19 deletions src/tox/tox_env/python/virtual_env/package/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -151,28 +160,29 @@ 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):
return w_env.perform_packaging(for_env)
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]
Expand Down Expand Up @@ -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
Expand All @@ -258,16 +272,17 @@ 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]:
return ["python"] + self.backend_args

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",
Expand Down
2 changes: 1 addition & 1 deletion tests/demo_pkg_setuptools/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[build-system]
requires = ["setuptools>=45", "wheel>=0.33"]
requires = ["setuptools>=63"]
build-backend = 'setuptools.build_meta'
6 changes: 3 additions & 3 deletions tests/session/cmd/test_sequential.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]
Expand Down
2 changes: 1 addition & 1 deletion tests/session/cmd/test_show_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""})
Expand All @@ -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")
Expand Down

0 comments on commit 6b36a88

Please sign in to comment.