# Developer Guide – Python Bindings This guide is aimed at contributors who want to extend or modify the Python bindings for `uLib`. --- ## Repository Layout ``` ulib/ ├── src/ │ └── Python/ │ ├── module.cpp # pybind11 module entry point │ ├── core_bindings.cpp # uLib::Core bindings │ ├── math_bindings.cpp # uLib::Math bindings │ ├── math_filters_bindings.cpp# VoxImageFilter bindings │ ├── CMakeLists.txt # builds uLib_python shared lib │ ├── testing/ # Python unit tests │ │ ├── pybind_test.py │ │ ├── core_pybind_test.py │ │ ├── math_pybind_test.py │ │ └── math_filters_test.py │ └── uLib/ # Python package (uLib_python.so lands here) │ └── __init__.py ├── build_python.py # poetry build hook (calls CMake) ├── pyproject.toml # poetry metadata └── condaenv.yml # conda/micromamba environment ``` --- ## Adding a New Binding All bindings live in the four source files listed above. The module entry point `module.cpp` calls `init_core()`, `init_math()`, and `init_math_filters()` in order. ### 1. Pick (or create) the right binding file | C++ header location | Binding file | |---|---| | `src/Core/` | `core_bindings.cpp` | | `src/Math/` (geometry, grids, VoxImage) | `math_bindings.cpp` | | `src/Math/VoxImageFilter*.hpp` | `math_filters_bindings.cpp` | ### 2. Add the `#include` directive ```cpp // math_bindings.cpp #include "Math/MyNewClass.h" ``` ### 3. Write the pybind11 binding inside the appropriate `init_*` function ```cpp void init_math(py::module_ &m) { // ... existing bindings ... py::class_(m, "MyNewClass") .def(py::init<>()) .def("MyMethod", &MyNewClass::MyMethod) .def("AnotherMethod", &MyNewClass::AnotherMethod, py::arg("x"), py::arg("y") = 0.0f); } ``` ### 4. Rebuild only the Python target ```bash cmake --build build --target uLib_python -j$(nproc) ``` ### 5. Write a Python test Add a new test class to the relevant test file (or create a new one under `src/Python/testing/`): ```python # src/Python/testing/math_pybind_test.py class TestMyNewClass(unittest.TestCase): def test_basic(self): obj = uLib.Math.MyNewClass() result = obj.MyMethod() self.assertAlmostEqual(result, expected_value) ``` Register the test in `src/Python/CMakeLists.txt` if you add a new file: ```cmake add_test(NAME pybind_my_new COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/my_new_test.py) set_tests_properties(pybind_my_new PROPERTIES ENVIRONMENT "PYTHONPATH=$:${PROJECT_SOURCE_DIR}/src/Python") ``` --- ## Build System Details ### CMakeLists.txt (`src/Python/`) `pybind11_add_module` compiles the shared library `uLib_python` and links it against the C++ static/shared libraries `uLibCore` and `uLibMath`. The install target copies the `.so` into the standard library directory. ```cmake pybind11_add_module(uLib_python module.cpp core_bindings.cpp math_bindings.cpp math_filters_bindings.cpp) target_link_libraries(uLib_python PRIVATE uLibCore uLibMath) ``` ### poetry / build_python.py `pyproject.toml` declares `build_python.py` as the custom build hook. When `poetry install` or `poetry build` is invoked it: 1. Calls `cmake -DCMAKE_LIBRARY_OUTPUT_DIRECTORY= ...` in `build_python/`. 2. Builds only the `uLib_python` target. 3. The resulting `.so` is placed inside `src/Python/uLib/` so it is picked up by Poetry as a package data file. The `USE_CUDA` environment variable gates CUDA support at build time: ```bash USE_CUDA=ON poetry install # with CUDA USE_CUDA=OFF poetry install # CPU only (default) ``` --- ## Running All Tests ```bash # From the repository root, with PYTHONPATH set: export PYTHONPATH="$(pwd)/build/src/Python:$(pwd)/src/Python" python -m pytest src/Python/testing/ -v ``` Or through CMake's test runner (after building the full project): ```bash cd build ctest --output-on-failure -R pybind ``` Expected output (all passing): ``` Start 1: pybind_general 1/4 Test #1: pybind_general ............. Passed Start 2: pybind_core 2/4 Test #2: pybind_core ................ Passed Start 3: pybind_math 3/4 Test #3: pybind_math ................ Passed Start 4: pybind_math_filters 4/4 Test #4: pybind_math_filters ........ Passed ``` --- ## Memory Management Notes `uLib::Vector` has explicit GPU memory management. When wrapping methods that return references to internal data, use `py::return_value_policy::reference_internal` to avoid dangling references: ```cpp .def("Data", &VoxImage::Data, py::return_value_policy::reference_internal) ``` For objects held by `std::unique_ptr` without Python-side deletion, use `py::nodelete`: ```cpp py::class_>(m, "AbstractVoxImageFilter") ``` --- ## Useful References - [pybind11 documentation](https://pybind11.readthedocs.io) - [pybind11 – STL containers](https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html) - [pybind11 – Eigen integration](https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html) - [CMake – pybind11 integration](https://pybind11.readthedocs.io/en/stable/compiling.html)