Skip to content

Commit

Permalink
Improve namespace package and generated module support for editable i…
Browse files Browse the repository at this point in the history
…nstalls
  • Loading branch information
tttapa committed Mar 26, 2024
1 parent 3fa51b9 commit 92addd6
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 19 deletions.
42 changes: 32 additions & 10 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""
Tests for the py-build-cmake package.
- Build all example projects
- Build all example and test projects
- Run the package's pytest tests
- Check the contents of the sdist and Wheel packages produced
- Build the component backend example
- Run the package's pytest tests
- Check the contents of the Wheel packages produced
- Test all three editable modes for the pybind11-project example
- Test all three (+1) editable modes for the example and test projects
- Install in editable mode and run the package's pytest
- Run the py-build-cmake pytest tests
"""
Expand Down Expand Up @@ -185,30 +185,52 @@ def component(session: nox.Session):
session.run("pytest")


def test_editable(session: nox.Session, mode: str):
def test_editable(
session: nox.Session, name: str, mode: str, dir: Path = Path("examples")
):
tmpdir = Path(session.create_tmp()).resolve()
m = mode.split("+", 1)
bh = str(len(m) > 1 and m[1] == "build_hook").lower()
skip_wrapper = ("namespace", "bare", "cmake-preset")
if m[0] == "wrapper" and any(k in name for k in skip_wrapper):
return
try:
with session.chdir("examples/pybind11-project"):
with session.chdir(dir / name):
shutil.rmtree(".py-build-cmake_cache", ignore_errors=True)
with (tmpdir / f"{mode}.toml").open("w") as f:
f.write(f'[editable]\nmode = "{mode}"')
session.install("-e", ".", "--config-settings=--local=" + f.name)
f.write(f'[editable]\nmode = "{m[0]}"\nbuild_hook = {bh}')
args = ("--config-settings=--local=" + f.name,)
if bh:
args += ("--no-build-isolation",)
session.install("-e", ".", *args)
session.run("pytest")
finally:
shutil.rmtree(tmpdir, ignore_errors=True)


@nox.session
def editable(session: nox.Session):
session.install("-U", "pip", "build", "pytest")
@nox.parametrize("mode", ["symlink", "symlink+build_hook", "hook", "wrapper"])
def editable(session: nox.Session, mode):
session.install(
"-U",
"pip",
"build",
"pytest",
"pybind11~=2.10.1",
"pybind11-stubgen~=0.16.2",
"nanobind~=1.5.1",
"nanobind-stubgen~=0.1.1",
)
dist_dir = os.getenv("PY_BUILD_CMAKE_WHEEL_DIR")
if dist_dir is None:
session.run("python", "-m", "build", ".")
dist_dir = "dist"
session.env["PIP_FIND_LINKS"] = str(Path(dist_dir).resolve())
session.install(f"py-build-cmake=={version}")
for mode in "wrapper", "hook", "symlink":
test_editable(session, mode)
for name in examples:
test_editable(session, name, mode)
for name in test_packages:
test_editable(session, name, mode, dir=Path("test-packages"))


@nox.session
Expand Down
9 changes: 4 additions & 5 deletions src/py_build_cmake/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,10 @@ def build_wheel_in_dir(
)

# Copy the module's Python source files to the temporary folder
if not module.is_generated:
if not editable:
export_util.copy_pkg_source_to(paths.staging_dir, module)
else:
paths = export_editable.do_editable_install(cfg, paths, module)
if not editable:
export_util.copy_pkg_source_to(paths.staging_dir, module)
else:
paths = export_editable.do_editable_install(cfg, paths, module)

# Create dist-info folder
distinfo_dir = f"{pkg_info.norm_name}-{pkg_info.version}.dist-info"
Expand Down
9 changes: 9 additions & 0 deletions src/py_build_cmake/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def iter_files_abs(self):
"""
assert self.is_package or not self.is_namespace

if self.is_generated:
# Generated modules/packages don't exist in the source directory
return

def _include(s: str | Path):
p = Path(s)
return p.name != "__pycache__" and not p.name.endswith(".pyc")
Expand Down Expand Up @@ -115,6 +119,11 @@ def check(self):
):
msg = "Namespace packages cannot use editable mode 'wrapper'"
raise ConfigError(msg)
if self.module.get("generated") and any(
e["mode"] == "wrapper" for e in self.editable.values()
):
msg = "Generated modules/packages cannot use editable mode 'wrapper'"
raise ConfigError(msg)


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion src/py_build_cmake/config/options/pyproject_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def get_options(project_path: Path, *, test: bool = False):
"Set to true for PEP 420 namespace packages.",
default=DefaultValueValue(False)),
EnumConfigOption("generated",
"Do not try to locate the module in the source "
"Do not try to locate the main module in the source "
"directory, but assume that it is generated by CMake. "
"Dynamic metadata cannot be used when set.",
options=["module", "package"]),
Expand Down
6 changes: 6 additions & 0 deletions src/py_build_cmake/export/editable/build_hook.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
from __future__ import annotations

import logging
import textwrap
from pathlib import Path

from ...commands.cmake import CMaker
from ...common import Config, Module
from ...common.util import get_os_name

logger = logging.getLogger(__name__)


def write_build_hook(cfg: Config, staging_dir: Path, module: Module, cmaker: CMaker):
"""Write a hook that re-compiles extension modules."""
edit_cfg = cfg.editable["cross" if cfg.cross else get_os_name()]
if not edit_cfg.get("build_hook"):
return
if edit_cfg.get("mode") != "symlink":
logger.warning("Skipping build_hook: only supported for symlink mode")
return
name = module.name
pkg_hook = staging_dir / (name + "_build_hook")
pkg_hook.mkdir(parents=True, exist_ok=True)
Expand Down
4 changes: 3 additions & 1 deletion src/py_build_cmake/export/editable/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ def __init__(self, name, extra_path):
def find_spec(self, name, path=None, target=None):
if name.split('.', 1)[0] != self.name:
return None
path = (path or []) + [self.extra_path]
if path is None:
path = []
path.append(self.extra_path)
return super().find_spec(name, path, target)
def install(name: str):
Expand Down
3 changes: 1 addition & 2 deletions src/py_build_cmake/export/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ def select_files(self):
include tests, docs, etc. for a 'gold standard' sdist.
"""
make_rel = lambda p: p.relative_to(self.module.base_path)
if not self.module.is_generated:
yield from map(make_rel, self.module.iter_files_abs())
yield from map(make_rel, self.module.iter_files_abs())
yield from map(make_rel, self.extra_files)

def crucial_files(self):
Expand Down

0 comments on commit 92addd6

Please sign in to comment.