From 7da1f6b2e98b4e3031454876f9e57397d737c654 Mon Sep 17 00:00:00 2001 From: "Leandro G. Almeida" Date: Sun, 31 May 2026 10:52:25 -0700 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20template:=20add=20C++=20Python?= =?UTF-8?q?=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repoman/copier/presets.py | 1 + src/repoman/cpp_template/copier.yml | 5 +++++ src/repoman/cpp_template/template/README.md.jinja | 9 ++++++++- .../docs/development/build-and-test.md.jinja | 7 +++++++ .../cpp_template/template/docs/index.md.jinja | 3 ++- .../cpp_template/template/meson.build.jinja | 15 +++++++++++++++ .../cpp_template/template/meson_options.txt.jinja | 1 + .../cpp_template/template/pyproject.toml.jinja | 1 + .../template/src/python_bindings.cpp.jinja | 13 +++++++++++++ .../cpp_template/template/tests/meson.build.jinja | 12 ++++++++++++ .../template/tests/test_python_bindings.py.jinja | 15 +++++++++++++++ 11 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/repoman/cpp_template/template/src/python_bindings.cpp.jinja create mode 100644 src/repoman/cpp_template/template/tests/test_python_bindings.py.jinja diff --git a/src/repoman/copier/presets.py b/src/repoman/copier/presets.py index 82974d9..62346d6 100644 --- a/src/repoman/copier/presets.py +++ b/src/repoman/copier/presets.py @@ -57,6 +57,7 @@ def build_preset_data(preset_name: str, project_name: str) -> dict: overrides.setdefault("cpp_build_cli", True) overrides.setdefault("cpp_build_examples", True) overrides.setdefault("cpp_build_tests", True) + overrides.setdefault("cpp_build_python_bindings", True) return {**base, **overrides} diff --git a/src/repoman/cpp_template/copier.yml b/src/repoman/cpp_template/copier.yml index 736e60e..b192ea3 100644 --- a/src/repoman/cpp_template/copier.yml +++ b/src/repoman/cpp_template/copier.yml @@ -130,3 +130,8 @@ cpp_build_tests: type: bool help: Build test executables default: true + +cpp_build_python_bindings: + type: bool + help: Build Python bindings + default: true diff --git a/src/repoman/cpp_template/template/README.md.jinja b/src/repoman/cpp_template/template/README.md.jinja index 06a3f61..fc203bd 100644 --- a/src/repoman/cpp_template/template/README.md.jinja +++ b/src/repoman/cpp_template/template/README.md.jinja @@ -40,11 +40,18 @@ make format make check ``` +Python bindings are built by default with pybind11 and expose a module named +`{{ cpp_namespace }}`. Disable them with: + +```bash +uv run meson configure build -Dbuild_python_bindings=false +``` + ## Layout ```text include/{{ cpp_namespace }}/ Public headers -src/ Library and CLI sources +src/ Library, CLI, and Python binding sources tests/ Meson test executables examples/ Small example programs docs/ ProperDocs documentation diff --git a/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja index ef11baa..4e619f5 100644 --- a/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja +++ b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja @@ -12,6 +12,13 @@ Build the project: make build ``` +The default build includes pybind11 Python bindings. You can disable them in an +existing build directory with: + +```bash +uv run meson configure build -Dbuild_python_bindings=false +``` + Run tests: ```bash diff --git a/src/repoman/cpp_template/template/docs/index.md.jinja b/src/repoman/cpp_template/template/docs/index.md.jinja index 525459e..9cf52f2 100644 --- a/src/repoman/cpp_template/template/docs/index.md.jinja +++ b/src/repoman/cpp_template/template/docs/index.md.jinja @@ -16,4 +16,5 @@ make build make test ``` -The public C++ API lives in `include/{{ cpp_namespace }}/`. +The public C++ API lives in `include/{{ cpp_namespace }}/`. Python bindings are +built by default as the `{{ cpp_namespace }}` module. diff --git a/src/repoman/cpp_template/template/meson.build.jinja b/src/repoman/cpp_template/template/meson.build.jinja index 6740649..dc91471 100644 --- a/src/repoman/cpp_template/template/meson.build.jinja +++ b/src/repoman/cpp_template/template/meson.build.jinja @@ -42,6 +42,21 @@ if get_option('build_cli') ) endif +if get_option('build_python_bindings') + python = import('python').find_installation() + pybind11_dep = dependency('pybind11') + + python.extension_module( + '{{ cpp_namespace }}', + 'src/python_bindings.cpp', + dependencies: [ + {{ cpp_library_name }}_dep, + pybind11_dep, + ], + install: true, + ) +endif + install_headers( 'include/{{ cpp_namespace }}/{{ cpp_namespace }}.hpp', subdir: '{{ cpp_namespace }}', diff --git a/src/repoman/cpp_template/template/meson_options.txt.jinja b/src/repoman/cpp_template/template/meson_options.txt.jinja index b96ccb9..579a441 100644 --- a/src/repoman/cpp_template/template/meson_options.txt.jinja +++ b/src/repoman/cpp_template/template/meson_options.txt.jinja @@ -1,3 +1,4 @@ option('build_cli', type: 'boolean', value: {{ "true" if cpp_build_cli else "false" }}, description: 'Build the example command-line executable') option('build_examples', type: 'boolean', value: {{ "true" if cpp_build_examples else "false" }}, description: 'Build example programs') option('build_tests', type: 'boolean', value: {{ "true" if cpp_build_tests else "false" }}, description: 'Build test executables') +option('build_python_bindings', type: 'boolean', value: {{ "true" if cpp_build_python_bindings else "false" }}, description: 'Build Python bindings') diff --git a/src/repoman/cpp_template/template/pyproject.toml.jinja b/src/repoman/cpp_template/template/pyproject.toml.jinja index 34a8ba7..b14dbdc 100644 --- a/src/repoman/cpp_template/template/pyproject.toml.jinja +++ b/src/repoman/cpp_template/template/pyproject.toml.jinja @@ -14,6 +14,7 @@ default-groups = ["ci", "docs"] ci = [ "meson>=1.5.0", "ninja>=1.11.1", + "pybind11>=2.13.6", ] docs = [ "markdown-callouts>=0.4.0", diff --git a/src/repoman/cpp_template/template/src/python_bindings.cpp.jinja b/src/repoman/cpp_template/template/src/python_bindings.cpp.jinja new file mode 100644 index 0000000..39b4a1e --- /dev/null +++ b/src/repoman/cpp_template/template/src/python_bindings.cpp.jinja @@ -0,0 +1,13 @@ +#include "{{ cpp_namespace }}/{{ cpp_namespace }}.hpp" + +#include + +namespace py = pybind11; + +PYBIND11_MODULE({{ cpp_namespace }}, module) { + module.doc() = "Python bindings for this C++ library."; + + module.def("version", &{{ cpp_namespace }}::version, "Return the library version."); + module.def("add", &{{ cpp_namespace }}::add, py::arg("lhs"), py::arg("rhs"), + "Add two integers."); +} diff --git a/src/repoman/cpp_template/template/tests/meson.build.jinja b/src/repoman/cpp_template/template/tests/meson.build.jinja index ba04510..028cb64 100644 --- a/src/repoman/cpp_template/template/tests/meson.build.jinja +++ b/src/repoman/cpp_template/template/tests/meson.build.jinja @@ -5,3 +5,15 @@ test_{{ cpp_library_name }} = executable( ) test('{{ cpp_library_name }}', test_{{ cpp_library_name }}) + +if get_option('build_python_bindings') + python_binding_env = environment() + python_binding_env.prepend('PYTHONPATH', meson.project_build_root()) + + test( + '{{ cpp_library_name }}_python', + python, + args: files('test_python_bindings.py'), + env: python_binding_env, + ) +endif diff --git a/src/repoman/cpp_template/template/tests/test_python_bindings.py.jinja b/src/repoman/cpp_template/template/tests/test_python_bindings.py.jinja new file mode 100644 index 0000000..df6e274 --- /dev/null +++ b/src/repoman/cpp_template/template/tests/test_python_bindings.py.jinja @@ -0,0 +1,15 @@ +"""Smoke tests for the generated Python bindings.""" + +from __future__ import annotations + +import importlib + + +def main() -> None: + module = importlib.import_module("{{ cpp_namespace }}") + assert module.add(2, 3) == 5 + assert module.version() == "0.1.0" + + +if __name__ == "__main__": + main() From b12103485fb6273036bc22eed13c410a0958b45b Mon Sep 17 00:00:00 2001 From: "Leandro G. Almeida" Date: Sun, 31 May 2026 10:52:38 -0700 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20tests:=20cover=20C++=20binding?= =?UTF-8?q?=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_copier.py | 1 + tests/test_template/test_cpp_template.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/tests/test_copier.py b/tests/test_copier.py index b84580d..cb5469b 100644 --- a/tests/test_copier.py +++ b/tests/test_copier.py @@ -86,6 +86,7 @@ def test_build_preset_data_cpp_preset() -> None: assert data["cpp_build_cli"] is True assert data["cpp_build_examples"] is True assert data["cpp_build_tests"] is True + assert data["cpp_build_python_bindings"] is True def test_build_copier_options_without_data() -> None: diff --git a/tests/test_template/test_cpp_template.py b/tests/test_template/test_cpp_template.py index 2a66167..8029637 100644 --- a/tests/test_template/test_cpp_template.py +++ b/tests/test_template/test_cpp_template.py @@ -36,24 +36,33 @@ def test_cpp_template_renders_spinach_style_structure(tmp_path: Path) -> None: "cpp_build_cli": True, "cpp_build_examples": True, "cpp_build_tests": True, + "cpp_build_python_bindings": True, }, ) assert (project_dir / "include" / "sample_cpp" / "sample_cpp.hpp").exists() assert (project_dir / "include" / "sample_cpp" / "version.hpp.in").exists() assert (project_dir / "src" / "sample_cpp.cpp").exists() + assert (project_dir / "src" / "python_bindings.cpp").exists() assert (project_dir / "src" / "cli.cpp").exists() assert (project_dir / "tests" / "test_sample_cpp.cpp").exists() + assert (project_dir / "tests" / "test_python_bindings.py").exists() assert (project_dir / "examples" / "basic.cpp").exists() assert (project_dir / "Doxyfile").exists() meson = _read_text(project_dir / "meson.build") + meson_options = _read_text(project_dir / "meson_options.txt") makefile = _read_text(project_dir / "Makefile") mkdocs = _read_text(project_dir / "config" / "mkdocs.yml") + pyproject = _read_text(project_dir / "pyproject.toml") github_ci = _read_text(project_dir / ".github" / "workflows" / "ci.yml") assert "subdir('include/sample_cpp')" in meson assert "'cpp_std=c++17'" in meson + assert "python.extension_module(" in meson + assert "dependency('pybind11')" in meson + assert "option('build_python_bindings'" in meson_options + assert '"pybind11>=2.13.6"' in pyproject assert "make check-docs" in github_ci assert "make check" in github_ci assert "make test" in github_ci From 1fb6d281378e4748fa4fe3481c547b2d086d1957 Mon Sep 17 00:00:00 2001 From: "Leandro G. Almeida" Date: Sun, 31 May 2026 10:52:47 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20docs:=20document=20C++=20bin?= =?UTF-8?q?ding=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/template-prompts.md | 17 +++++++++-------- docs/template-structure.md | 5 ++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/template-prompts.md b/docs/template-prompts.md index 3b3a6e5..1c65760 100644 --- a/docs/template-prompts.md +++ b/docs/template-prompts.md @@ -61,11 +61,12 @@ The bundled C++ template lives in `src/repoman/cpp_template/` and is selected with `--preset cpp` or `repoman create cpp cpp`. In addition to project, author, repository, and license metadata, it prompts for: -| Prompt | Description | Default | -| -------------------- | -------------------------------------- | ------------------------- | -| `cpp_namespace` | C++ namespace and public include dir | project name with `_` | -| `cpp_library_name` | Meson library target name | project name with `_` | -| `cpp_standard` | C++ language standard | `c++17` | -| `cpp_build_cli` | Build the example CLI executable | `true` | -| `cpp_build_examples` | Build example programs | `true` | -| `cpp_build_tests` | Build test executables | `true` | +| Prompt | Description | Default | +| --------------------------- | -------------------------------------- | --------------------- | +| `cpp_namespace` | C++ namespace and public include dir | project name with `_` | +| `cpp_library_name` | Meson library target name | project name with `_` | +| `cpp_standard` | C++ language standard | `c++17` | +| `cpp_build_cli` | Build the example CLI executable | `true` | +| `cpp_build_examples` | Build example programs | `true` | +| `cpp_build_tests` | Build test executables | `true` | +| `cpp_build_python_bindings` | Build Python bindings with pybind11 | `true` | diff --git a/docs/template-structure.md b/docs/template-structure.md index 4c6df75..60a402f 100644 --- a/docs/template-structure.md +++ b/docs/template-structure.md @@ -9,7 +9,7 @@ - **Docs-only:** If `docs_only` is true, source package and test files are omitted. - **Notebooks:** If `python_notebooks` is true, the template generates a `notebooks/` folder. - **Paper:** If `latex_paper` is true, the template generates a `paper/` folder with a LaTeX manuscript scaffold and Make targets. -- **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, Doxygen hooks, and the same repoman Make target names used by CI. +- **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, optional pybind11 Python bindings, Doxygen hooks, and the same repoman Make target names used by CI. ## High-level generated layout @@ -58,5 +58,8 @@ my_cpp_library/ │ └── / ├── scripts/ ├── src/ +│ ├── .cpp +│ ├── cli.cpp +│ └── python_bindings.cpp └── tests/ ```