From 9f643d08d01be2d704666b58d10df511a23bcec4 Mon Sep 17 00:00:00 2001 From: Clemens Kubach Date: Mon, 30 Mar 2026 21:56:18 +0200 Subject: [PATCH] fix include/exclude compiled_src order as described in README.md --- src/hatch_cython/__about__.py | 2 +- src/hatch_cython/plugin.py | 25 +++- .../namespace_structure/LICENSE.txt | 0 test_libraries/namespace_structure/hatch.toml | 12 ++ .../namespace_structure/pyproject.toml | 11 ++ .../src/org/myproject/__about__.py | 1 + .../src/org/myproject/__init__.py | 0 .../src/org/myproject/core.pyx | 2 + tests/test_plugin.py | 138 ++++++++++++++++++ 9 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 test_libraries/namespace_structure/LICENSE.txt create mode 100644 test_libraries/namespace_structure/hatch.toml create mode 100644 test_libraries/namespace_structure/pyproject.toml create mode 100644 test_libraries/namespace_structure/src/org/myproject/__about__.py create mode 100644 test_libraries/namespace_structure/src/org/myproject/__init__.py create mode 100644 test_libraries/namespace_structure/src/org/myproject/core.pyx diff --git a/src/hatch_cython/__about__.py b/src/hatch_cython/__about__.py index f361411..25f8e0d 100644 --- a/src/hatch_cython/__about__.py +++ b/src/hatch_cython/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present joshua-auchincloss # # SPDX-License-Identifier: MIT -__version__ = "0.6.0" +__version__ = "0.6.1" diff --git a/src/hatch_cython/plugin.py b/src/hatch_cython/plugin.py index f15ff64..955f448 100644 --- a/src/hatch_cython/plugin.py +++ b/src/hatch_cython/plugin.py @@ -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 @@ -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) diff --git a/test_libraries/namespace_structure/LICENSE.txt b/test_libraries/namespace_structure/LICENSE.txt new file mode 100644 index 0000000..e69de29 diff --git a/test_libraries/namespace_structure/hatch.toml b/test_libraries/namespace_structure/hatch.toml new file mode 100644 index 0000000..8d3ce38 --- /dev/null +++ b/test_libraries/namespace_structure/hatch.toml @@ -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"] diff --git a/test_libraries/namespace_structure/pyproject.toml b/test_libraries/namespace_structure/pyproject.toml new file mode 100644 index 0000000..968e6c5 --- /dev/null +++ b/test_libraries/namespace_structure/pyproject.toml @@ -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" diff --git a/test_libraries/namespace_structure/src/org/myproject/__about__.py b/test_libraries/namespace_structure/src/org/myproject/__about__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/test_libraries/namespace_structure/src/org/myproject/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/test_libraries/namespace_structure/src/org/myproject/__init__.py b/test_libraries/namespace_structure/src/org/myproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_libraries/namespace_structure/src/org/myproject/core.pyx b/test_libraries/namespace_structure/src/org/myproject/core.pyx new file mode 100644 index 0000000..11667cd --- /dev/null +++ b/test_libraries/namespace_structure/src/org/myproject/core.pyx @@ -0,0 +1,2 @@ +def hello(): + return "hello from org.myproject" diff --git a/tests/test_plugin.py b/tests/test_plugin.py index a2d082d..4a1024a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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 @@ -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): @@ -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))