From 7b31b33cfa382079ee2f21e14a0e96ffcb52233b Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Fri, 30 Oct 2026 20:09:34 +0800 Subject: [PATCH 1/2] fix: a checkout is a built source tree only when the compiled module is there, not when the type stubs are The validation workers (build123dftc, cadqueryftc) put the checkout's src on their subprocess's path only when it is a built tree, because it would shadow the installed wheel with Python files that have no compiled module. They looked for glob('_native*'), which the new type stub directory src/simplecadapi/_cpp/_native/ matches: in the release workflow's wheel tests (the wheel installed, the checkout not built) six tests failed with 'simplecadapi needs its compiled C++ module'. wheel_smoke.py asserted the same glob, which the stubs would have satisfied without the extension. All three now require a file. A test pins the rule (stubs alone: not built; .so / .pyd: built). Co-Authored-By: Claude Sonnet 5.5 --- src/cpp/tests/wheel_smoke.py | 3 +- test/test_source_tree_is_built.py | 45 +++++++++++++++++++ .../research/build123dftc/b3ftc/validation.py | 12 ++++- .../research/cadqueryftc/cadquery_validate.py | 5 ++- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 test/test_source_tree_is_built.py diff --git a/src/cpp/tests/wheel_smoke.py b/src/cpp/tests/wheel_smoke.py index c81cf5da..a9ed4882 100644 --- a/src/cpp/tests/wheel_smoke.py +++ b/src/cpp/tests/wheel_smoke.py @@ -21,7 +21,8 @@ def main() -> None: # The extension is the one shipped in the wheel, and it carries its own OpenCASCADE (no OCP, no system OCCT). here = Path(cpp.__file__).resolve().parent - assert any(here.glob("_native*")), f"no compiled extension next to {cpp.__file__}" + # a file: a directory `_native/` next to it holds the type stubs and is not the extension + assert any(path.is_file() for path in here.glob("_native*")), f"no compiled extension next to {cpp.__file__}" from simplecadapi._cpp import occt print("python", sys.version.split()[0], "| OpenCASCADE", occt.__version__, "|", here) diff --git a/test/test_source_tree_is_built.py b/test/test_source_tree_is_built.py new file mode 100644 index 00000000..4e395ee7 --- /dev/null +++ b/test/test_source_tree_is_built.py @@ -0,0 +1,45 @@ +"""A checkout is a built source tree only when the compiled module is there; the type stubs do not count. + +The checkout carries `src/simplecadapi/_cpp/_native/*.pyi` (a directory). The validation workers put the checkout's +source on the path of their subprocess only when it is built, because otherwise it shadows the installed wheel with Python +files that have no compiled module: the wheel test job of the 2.2.7 dry run failed that way. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools" / "research" / "build123dftc")) +from b3ftc.validation import _source_tree_is_built # noqa: E402 + + +def make(tmp_path: Path, *names: str, stubs: bool = False) -> Path: + cpp = tmp_path / "simplecadapi" / "_cpp" + cpp.mkdir(parents=True) + if stubs: + (cpp / "_native").mkdir() + (cpp / "_native" / "__init__.pyi").write_text("") + for name in names: + (cpp / name).write_bytes(b"") + return tmp_path + + +def test_the_type_stub_directory_alone_is_not_a_build(tmp_path) -> None: + assert not _source_tree_is_built(make(tmp_path, stubs=True)) + + +def test_nothing_is_not_a_build(tmp_path) -> None: + assert not _source_tree_is_built(make(tmp_path)) + + +@pytest.mark.parametrize("name", ["_native.cpython-312-darwin.so", "_native.cpython-313-x86_64-linux-gnu.so", "_native.cp312-win_amd64.pyd"]) +def test_the_compiled_module_is_a_build_with_or_without_stubs(tmp_path, name) -> None: + assert _source_tree_is_built(make(tmp_path, name, stubs=True)) + + +def test_the_real_checkout_is_built_here() -> None: + """The test suite runs against a built tree (the extension is next to its Python files).""" + assert _source_tree_is_built(Path(__file__).resolve().parents[1] / "src") diff --git a/tools/research/build123dftc/b3ftc/validation.py b/tools/research/build123dftc/b3ftc/validation.py index 70bbf727..0c0ae925 100644 --- a/tools/research/build123dftc/b3ftc/validation.py +++ b/tools/research/build123dftc/b3ftc/validation.py @@ -159,6 +159,16 @@ def _interpreter(executable: str | None) -> str: return os.path.abspath(executable) if os.sep in executable else executable +def _source_tree_is_built(source: Path) -> bool: + """Whether the compiled module sits next to the Python files of *source* (a development build). + + The module is a file (`_native*.so` / `_native*.pyd`); a *directory* `_native/` next to it holds the type stubs a + checkout carries and says nothing about a build. + """ + + return any(path.is_file() for path in (source / "simplecadapi" / "_cpp").glob("_native*")) + + def _process(command: list[str], staging: Path, env: Mapping[str, str], output: Path, timeout: float) -> dict[str, Any]: if timeout <= 0: @@ -222,7 +232,7 @@ def _worker(ftc_path: Path, workdir: Path | None, timeout: float, # The checkout's source tree goes on the path only when it is a built one (the compiled module sits next to # its Python files, as in a development build); otherwise it would shadow the installed wheel. source = repo_root / "src" - built = any((source / "simplecadapi" / "_cpp").glob("_native*")) + built = _source_tree_is_built(source) env["PYTHONPATH"] = os.pathsep.join( [*([str(source)] if built else []), str(tool_dir), env.get("PYTHONPATH", "")]) started = time.monotonic() diff --git a/tools/research/cadqueryftc/cadquery_validate.py b/tools/research/cadqueryftc/cadquery_validate.py index a613aa63..b9e88757 100644 --- a/tools/research/cadqueryftc/cadquery_validate.py +++ b/tools/research/cadqueryftc/cadquery_validate.py @@ -32,7 +32,10 @@ if str(_TOOLS_DIR) not in sys.path: sys.path.insert(0, str(_TOOLS_DIR)) # the checkout's source tree only when it is a built one (it would shadow an installed wheel otherwise) -if str(_REPO_ROOT / "src") not in sys.path and any((_REPO_ROOT / "src" / "simplecadapi" / "_cpp").glob("_native*")): +# (the module is a file; a directory `_native/` next to it holds the type stubs and is no sign of a build) +if str(_REPO_ROOT / "src") not in sys.path and any( + path.is_file() for path in (_REPO_ROOT / "src" / "simplecadapi" / "_cpp").glob("_native*") +): sys.path.insert(0, str(_REPO_ROOT / "src")) from cqftc.tracer.trace_schema import OperationTrace # noqa: E402 From e64224c554da7659bd9a3185f9bcb2ed70ce18ba Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Fri, 30 Oct 2026 20:31:24 +0800 Subject: [PATCH 2/2] test: drop the assertion that the checkout is built (true only in a development build, false in CI's editable install) Co-Authored-By: Claude Sonnet 5.5 --- test/test_source_tree_is_built.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_source_tree_is_built.py b/test/test_source_tree_is_built.py index 4e395ee7..0a4f6318 100644 --- a/test/test_source_tree_is_built.py +++ b/test/test_source_tree_is_built.py @@ -39,7 +39,3 @@ def test_nothing_is_not_a_build(tmp_path) -> None: def test_the_compiled_module_is_a_build_with_or_without_stubs(tmp_path, name) -> None: assert _source_tree_is_built(make(tmp_path, name, stubs=True)) - -def test_the_real_checkout_is_built_here() -> None: - """The test suite runs against a built tree (the extension is next to its Python files).""" - assert _source_tree_is_built(Path(__file__).resolve().parents[1] / "src")