Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/hatch_cython/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present joshua-auchincloss <joshua.auchincloss@proton.me>
#
# SPDX-License-Identifier: MIT
__version__ = "0.6.0"
__version__ = "0.6.1"
25 changes: 23 additions & 2 deletions src/hatch_cython/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,10 @@ def autogenerated_files(self) -> RelAbsPathMap:
def inclusion_map(self):
include = {}
for compl in list(self.compiled_files.keys()):
include[compl] = compl
dest = compl
if self.is_src and dest.startswith("src/"):
dest = dest[4:] # strip "src/" for correct wheel placement
include[compl] = dest
self.app.display_debug("Derived inclusion map")
self.app.display_debug(include)
return include
Expand Down Expand Up @@ -565,7 +568,25 @@ def should_drop(member_name: str) -> bool:
return False

# IMPORTANT: use your existing pathspec-based matchers
return self.path_is_excluded_compiled_src(name) and not self.path_is_included_compiled_src(name)
# For src-layout, config patterns may use "src/" prefix
# but wheel paths don't have it. Try both.
is_excluded = self.path_is_excluded_compiled_src(name)
is_included = self.path_is_included_compiled_src(name)
if self.is_src:
src_name = f"src/{name}"
is_excluded = is_excluded or self.path_is_excluded_compiled_src(src_name)
is_included = is_included or self.path_is_included_compiled_src(src_name)

# Mirror path_is_wanted_excluded_compiled_src: when include_all_compiled_src
# is False, treat every compiled .py as excluded (unless in include_compiled_src)
if not self.options.include_all_compiled_src:
is_compiled = self.path_is_wanted(name)
if self.is_src:
is_compiled = is_compiled or self.path_is_wanted(f"src/{name}")
if is_compiled:
is_excluded = True

return is_excluded and not is_included

tmp_fd, tmp_path = tempfile.mkstemp(suffix=".whl")
os.close(tmp_fd)
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions test_libraries/namespace_structure/hatch.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[build]
ignore-vcs = true

[build.hooks.custom]
dependencies = ["Cython", "setuptools", "hatch"]
path = "../../src/hatch_cython/devel.py"

[build.hooks.custom.options]
src = "org/myproject"

[build.targets.wheel]
packages = ["src/org"]
11 changes: 11 additions & 0 deletions test_libraries/namespace_structure/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling", "Cython", "setuptools"]

[project]
name = "org-myproject"
dynamic = ["version"]
requires-python = ">=3.8"

[tool.hatch.version]
path = "src/org/myproject/__about__.py"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
Empty file.
2 changes: 2 additions & 0 deletions test_libraries/namespace_structure/src/org/myproject/core.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def hello():
return "hello from org.myproject"
138 changes: 138 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import gc
import io
import shutil
import tempfile
import zipfile
from os import getcwd, path
from pathlib import Path
from sys import path as syspath
Expand Down Expand Up @@ -38,6 +41,73 @@ def new_src_proj(tmp_path):
return project_dir


@pytest.fixture
def new_namespace_proj(tmp_path):
project_dir = tmp_path / "app"
project_dir.mkdir()
(project_dir / "pyproject.toml").write_text(read("test_libraries/namespace_structure/pyproject.toml"))
(project_dir / "hatch.toml").write_text(read("test_libraries/namespace_structure/hatch.toml"))
(project_dir / "LICENSE.txt").write_text("")
shutil.copytree(join("test_libraries/namespace_structure", "src"), (project_dir / "src"))
return project_dir


def test_namespace_build_hook(new_namespace_proj):
with override_dir(new_namespace_proj):
syspath.insert(0, str(new_namespace_proj))
build_config = load(new_namespace_proj / "hatch.toml")["build"]
cython_config = build_config["hooks"]["custom"]
builder = WheelBuilder(root=str(new_namespace_proj))
hook = CythonBuildHook(
new_namespace_proj,
cython_config,
WheelBuilderConfig(
builder=builder,
root=str(new_namespace_proj),
plugin_name="cython",
build_config=build_config,
target_config=build_config["targets"]["wheel"],
),
SimpleNamespace(name="org-myproject"),
directory=new_namespace_proj,
target_name="wheel",
)

assert hook.is_src
assert hook.dir_name == "org/myproject"
assert hook.project_dir == "src/org/myproject"

assert sorted(hook.precompiled_globs) == sorted(
[
"src/org/myproject/*.py",
"src/org/myproject/**/*.py",
"src/org/myproject/*.pyx",
"src/org/myproject/**/*.pyx",
"src/org/myproject/*.pxd",
"src/org/myproject/**/*.pxd",
]
)

assert sorted(hook.normalized_included_files) == sorted(
[
"src/org/myproject/__about__.py",
"src/org/myproject/__init__.py",
"src/org/myproject/core.pyx",
]
)

assert sorted(
[{**ls, "files": sorted(ls.get("files"))} for ls in hook.grouped_included_files],
key=lambda x: x.get("name"),
) == [
{"name": "org.myproject.__about__", "files": ["src/org/myproject/__about__.py"]},
{"name": "org.myproject.__init__", "files": ["src/org/myproject/__init__.py"]},
{"name": "org.myproject.core", "files": ["src/org/myproject/core.pyx"]},
]

syspath.remove(str(new_namespace_proj))


@pytest.mark.parametrize("include_all_compiled_src", [None, True, False])
def test_wheel_build_hook(new_src_proj, include_all_compiled_src: bool | None):
with override_dir(new_src_proj):
Expand Down Expand Up @@ -238,3 +308,71 @@ def make_hook():
assert len(so_files_after) == 0, f"compiled extension files not removed by clean(): {so_files_after}"

syspath.remove(str(new_src_proj))


def test_finalize_drops_compiled_src_when_include_all_false(new_src_proj):
"""finalize() must remove compiled .py files from the wheel when include_all_compiled_src=False.

Files listed in include_compiled_src should survive; everything else that was
compiled should be dropped.
"""
with override_dir(new_src_proj):
syspath.insert(0, str(new_src_proj))
build_config = load(new_src_proj / "hatch.toml")["build"]
cython_config = build_config["hooks"]["custom"]
cython_config["options"]["include_all_compiled_src"] = False

builder = WheelBuilder(root=str(new_src_proj))
hook = CythonBuildHook(
new_src_proj,
cython_config,
WheelBuilderConfig(
builder=builder,
root=str(new_src_proj),
plugin_name="cython",
build_config=build_config,
target_config=build_config["targets"]["wheel"],
),
SimpleNamespace(name="example_lib"),
directory=new_src_proj,
target_name="wheel",
)

# Wheel paths use no "src/" prefix (hatchling strips it for src-layout).
# These are all compiled files; normal_include_compiled_src.py is the only
# one listed in include_compiled_src and should survive.
wheel_py_files = [
"example_lib/__about__.py",
"example_lib/__init__.py",
"example_lib/normal.py",
"example_lib/normal_exclude_compiled_src.py",
"example_lib/normal_include_compiled_src.py",
]
record_name = "example_lib-0.1.0.dist-info/RECORD"

# Build a minimal fake wheel zip
with tempfile.NamedTemporaryFile(suffix=".whl", delete=False) as tmp:
whl_path = tmp.name
with zipfile.ZipFile(whl_path, "w") as zf:
for f in wheel_py_files:
zf.writestr(f, b"# placeholder")
zf.writestr(record_name, "")

build_data: dict = {"artifacts": [], "force_include": {}}
hook.finalize("0.1.0", build_data, whl_path)

with zipfile.ZipFile(whl_path) as zf:
names = set(zf.namelist())

# normal_include_compiled_src.py is in include_compiled_src → must stay
assert "example_lib/normal_include_compiled_src.py" in names
# all other compiled .py files must be dropped
for dropped in [
"example_lib/__about__.py",
"example_lib/__init__.py",
"example_lib/normal.py",
"example_lib/normal_exclude_compiled_src.py",
]:
assert dropped not in names, f"{dropped!r} should have been dropped from wheel"

syspath.remove(str(new_src_proj))
Loading