Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP-660 support #2502

Merged
merged 1 commit into from
Nov 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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")]}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems setuptools dropped this flag 🤷

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