From aa47f7facafe5086fdefaf84906b954705a2f5de Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 6 Nov 2026 21:23:05 +0000 Subject: [PATCH 1/8] docs(holoscan-modules): add the pure-Python Module workflow to Create a Module Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tutorials/holoscan-modules/README.md | 16 +- .../create-a-module/README.md | 237 +++++++++++++++--- .../create-a-module/metadata.json | 3 +- 3 files changed, 216 insertions(+), 40 deletions(-) diff --git a/tutorials/holoscan-modules/README.md b/tutorials/holoscan-modules/README.md index 9771f8f..3d998ec 100644 --- a/tutorials/holoscan-modules/README.md +++ b/tutorials/holoscan-modules/README.md @@ -17,17 +17,17 @@ Browse published modules in the [Modules tab](https://nvidia-holoscan.github.io/ ## Requirements - Holoscan SDK 5.0.0 or later with CUDA 13 -- CMake 4.0 or later and a C++20-capable compiler -- Python 3.12 through 3.14 for the `holoscan-cli==5.0.0` commands used by this walkthrough +- CMake 4.0 or later and a C++20-capable compiler for Modules with C++ operators +- Python 3.12 through 3.14 for the `holoscan-cli` commands used by these walkthroughs - Docker, Docker Buildx, and NVIDIA Container Toolkit for the recommended container workflow -SDK 5.0 Module operators are implemented in C++. The scaffold can add Python bindings so Python -applications can use them, but it does not generate Python-implemented operators or wheel packages. -The packaged artifact is a Debian package. +Module operators are implemented in C++ or Python. For C++ operators, the scaffold can add Python +bindings so Python applications can use them, and the packaged artifact is a Debian package. +From Holoscan CLI `5.1.0`, `holoscan create --language python` creates a pure-Python Module that +ships as a Python wheel and needs no C++ compiler. -The examples use `holoscan-cli==5.0.0` from PyPI, the version pinned by the current SDK 5 -template. CLI and SDK versions are independent: the scaffold uses CLI `5.0.0` while requiring -SDK `5.0.0` or later. See the +The examples install `holoscan-cli` from PyPI; each tutorial pins the CLI version it uses. CLI and +SDK versions are independent: the scaffold requires SDK `5.0.0` or later. See the [Holoscan CLI documentation](https://github.com/nvidia-holoscan/holoscan-cli) for configuration and integration details. diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index ae7ca73..41dd306 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -3,7 +3,8 @@ In this tutorial, we'll create a standalone **Holoscan Module** for Holoscan SDK 5.0. We'll start with an empty directory, build a reusable C++ operator, connect it to a finite example graph, optionally call it from Python, exercise it with deterministic tests, and finish with a Debian -package that another Holoscan project can install. +package that another Holoscan project can install. If you'd rather write the operators in Python, +[section 4.6](#46-implement-operators-in-pure-python) shows a pure-Python Module with no C++ code. By the end, you'll have an independently versioned repository and a repeatable development workflow. @@ -30,9 +31,10 @@ is not a reusable library does not need to become a Module. This tutorial creates a **standalone Module**: an independent `holoscan-` repository with its own build, tests, package, and CI. -In Holoscan SDK 5.0, Module operators are implemented in C++. The scaffold can also add Python bindings: a -small extension that registers the C++ operators with the SDK operator registry so Python -applications can use them. It does not generate Python-implemented operators or wheels. +Module operators are implemented in either C++ or Python. For C++ operators, the scaffold can +also add Python bindings: a small extension that registers the C++ operators with the SDK +operator registry so Python applications can use them. For Python operators, it creates a +pure-Python Module that needs no C++ code or compiler and ships as a Python wheel. ### Who creates a Holoscan Module? @@ -44,7 +46,8 @@ You may want to create one when: - you need a reproducible Debian package tied to a known Holoscan SDK version. In each case, the goal is the same: give consumers a stable CMake target and APT installation -experience without asking them to copy source files or understand your repository internals. +for C++ operators, or a `pip install` and `import holoscan.` for Python operators, without +asking them to copy source files or understand your repository internals. ### Why use the Module scaffold? @@ -88,8 +91,15 @@ pytest, and the Holoscan SDK Python package for the same SDK version, importable The SDK Debian packages don't include the Python package; the development container does. CMake downloads nanobind 3.0.1 while configuring when it isn't installed, which needs network access. -The CLI version does not select the SDK version. This tutorial pins Holoscan CLI `5.0.0` and -explicitly selects Holoscan SDK `5.0.0`. +A pure-Python Module ([section 4.6](#46-implement-operators-in-pure-python)) needs much less on +the host: Python 3.12 or later on Linux with glibc 2.39 or newer (for example Ubuntu 24.04), a +driver that supports CUDA 13, and CMake 3.24 or later for the CLI lifecycle commands. Its pip +dependencies install the Holoscan SDK `holoscan-core-cu13` wheel, so it needs no SDK Debian +package, CUDA Toolkit, compiler, or GoogleTest. + +The CLI version does not select the SDK version. This tutorial pins Holoscan CLI `5.1.0` and +explicitly selects Holoscan SDK `5.0.0`. Holoscan CLI `5.1.0` is the first release with the +pure-Python Module template. ## 3. Create an External Module @@ -108,7 +118,7 @@ interactive setup: ```bash python3.12 -m venv .cli-venv source .cli-venv/bin/activate -python -m pip install 'holoscan-cli[create]==5.0.0' +python -m pip install 'holoscan-cli[create]==5.1.0' holoscan create my-module # simplified command for interactive setup ``` @@ -118,14 +128,14 @@ write anything to disk, let's preview exactly what the CLI plans to create. With `uv`: ```bash -uvx --python 3.12 --from 'holoscan-cli[create]==5.0.0' \ +uvx --python 3.12 --from 'holoscan-cli[create]==5.1.0' \ holoscan create my-module \ --language cpp \ --interactive false \ --context holoscan_version=5.0.0 \ --dryrun -uvx --python 3.12 --from 'holoscan-cli[create]==5.0.0' \ +uvx --python 3.12 --from 'holoscan-cli[create]==5.1.0' \ holoscan create my-module \ --language cpp \ --interactive false \ @@ -160,8 +170,15 @@ To also use the operators from Python, pass `--language cpp python` instead. Thi bindings for the C++ operator, a Python version of the example, and a `pytest` suite; [section 4.5](#45-call-the-operator-from-python) walks through them. -Note that `--language python` on its -own is rejected because Holoscan SDK 5.0 does not yet support Python-implemented operators. +To write the operators in Python instead, pass `--language python`. This creates a pure-Python +Module with no C++ code; [section 4.6](#46-implement-operators-in-pure-python) walks through it. +A Module's operators are either all C++ or all Python: + +| `--language` | Operators | Use from Python | Package | +| --- | --- | --- | --- | +| `cpp` | C++ | No | Debian | +| `cpp python` | C++ | Through generated Python bindings | Debian | +| `python` | Python | Directly | Python wheel | ### 3.2 Set up the generated development environment @@ -238,6 +255,32 @@ operator names, supported platforms, and `holoscan_sdk.minimum_required_version` metadata declares a C++ namespace and a Debian binary package; with Python bindings it also lists `Python` and the `holoscan_my_module` Python namespace. +A Module created with `--language python` has no C++ sources, C++ tests, or Debian package +configuration. Its operators live in the Python package, and its CMake project only stages that +package and runs the tests: + +```text +holoscan-my-module/ +├── metadata.json # language ["Python"], Python namespace, wheel package +├── pyproject.toml # Wheel build (scikit-build-core), CLI pin, ruff +├── CMakeLists.txt # CMake 3.24, no compiled languages +├── Dockerfile # SDK 5.0 CUDA 13 development image +├── python/ +│ ├── CMakeLists.txt # Stages holoscan_my_module in the build tree +│ └── holoscan_my_module/ +│ └── operators/ +│ └── my_module_op.py # Typed Input/Output class attributes +├── operators/ +│ └── my_module_op/ +│ └── metadata.json # Operator metadata only +├── applications/ +│ └── my_module_pipeline/ +│ └── python/ # Graph → compile → run_async example +├── tests/ +│ └── python/ # OperatorTestHarness and pipeline tests +└── .github/workflows/ci.yml +``` + ## 4. Implement, Build, and Test The scaffold gives us a working pass-through example. Next we'll turn that example into the @@ -447,7 +490,123 @@ PYTHONPATH=build/my_module_pipeline/python/lib:$PYTHONPATH \ For a native Python build, use `build-native/my_module_pipeline/python/lib` instead. -### 4.6 Build and test natively +### 4.6 Implement operators in pure Python + +If you generated the Module with `--language python`, its operators are Python classes in the +`holoscan_my_module` package and there is no C++ to build. They use the same SDK 5 model as the +C++ operator: typed ports, a `compute()` method, and a graph that you compose, compile, and run. + +The generated operator in `python/holoscan_my_module/operators/my_module_op.py` declares its ports +as class attributes and forwards each value: + +```python +import holoscan as hs + + +class MyModuleOp(hs.Operator): + """Forward each received value unchanged.""" + + inp = hs.Input("in", hs.payload.uint32) + out = hs.Output("out", hs.payload.uint32) + + def compute(self, context: hs.ExecutionContext) -> None: + self.out.emit(self.inp.receive_data()) +``` + +With one connected input and no `contract()` override, `compute()` runs once per received value. +Replace `hs.payload.uint32` with your payload type, and add parameters as `hs.Param` class +attributes. The example application composes the operator into a finite graph between a clocked +source and a counting sink: + +```python +import holoscan as hs +from holoscan_my_module.operators import MyModuleOp + +graph = hs.Graph("my_module_pipeline", default_clock=hs.RealtimeClock) +source = graph.op(CounterSource, "source") +forward = graph.op(MyModuleOp, "my_module_op") +sink = graph.op(CountingSink, "sink") +graph.add_flow(source.output, forward["in"]) # `in` is a Python keyword +graph.add_flow(forward.out, sink.input) + +plan = hs.compile(graph) +plan.require_no_errors() +``` + +Build, run, and test it with the same commands as section 4.3. The example has only a Python +variant, so `--language` isn't needed. `holoscan build` compiles nothing: it stages the package in +the build tree, and `holoscan run` puts that directory on `PYTHONPATH`. + +```bash +holoscan build my_module_pipeline --dryrun --verbose +holoscan build my_module_pipeline --verbose + +holoscan run my_module_pipeline --dryrun --verbose +holoscan run my_module_pipeline --verbose + +holoscan test --dryrun --verbose +holoscan test --verbose +``` + +The run prints `my_module_pipeline received 10 samples`. `holoscan test` runs the `tests/python` +pytest suite through CTest. The suite checks that `import holoscan.my_module` resolves to the +package, runs the example, and tests the operator on finite input with +`holoscan.testing.OperatorTestHarness`: + +```python +from holoscan.testing import OperatorTestHarness + + +def test_operator_forwards_each_value(): + with OperatorTestHarness(MyModuleOp) as harness: + harness.push_many("in", [1, 2, 3]) + result = harness.run() + assert result.values("out") == [1, 2, 3] +``` + +Replace the pass-through assertions with checks for your operator's real transformation. To add +an operator, add a module under `python/holoscan_my_module/operators/`, export it from +`operators/__init__.py`, describe it in `operators//metadata.json` and +`metadata.json:module.operator_names`, and cover it in `tests/python`. + +To use the build tree from a Python shell or notebook, register it with a development hook from +the development container (`holoscan run-container`) or a host with the SDK wheels: + +```bash +holoscan install --dev --local +python3 -c "import holoscan.my_module; print(holoscan.my_module.__file__)" +holoscan install --dev --uninstall --local +``` + +The hook writes a `.pth` file that points at the staged package, so rebuilding picks up source +edits without reinstalling. + +You don't need the CLI to develop a pure-Python Module. On a host that meets the pure-Python +requirements in section 2, pip installs the Module in editable mode along with the SDK wheels it +depends on: + +```bash +python3 -m venv .venv-module +source .venv-module/bin/activate +python -m pip install -e . pytest +python -m pytest tests/python +``` + +When the operators are ready, build the wheel: + +```bash +holoscan package holoscan-my-module --pkg-generator WHEEL --dryrun --verbose +holoscan package holoscan-my-module --pkg-generator WHEEL --verbose +``` + +The wheel is written to `build/dist/holoscan_my_module-0.1.0-py3-none-any.whl`. It contains only +the Python package and the `holoscan.modules` entry point that makes `import holoscan.my_module` +work, and it depends on `holoscan-core-cu13`. If your operators import other SDK packages, add +`holoscan-vision-cu13`, `holoscan-visualization-cu13`, or `holoscan-inference-cu13` to +`[project].dependencies` in `pyproject.toml`. A pure-Python Module has no Debian package, so skip +section 4.9. + +### 4.7 Build and test natively The container is the easiest way to keep the toolchain reproducible. If your host already has a compatible SDK, CUDA toolkit, compiler, and CMake, you can use native mode for a faster local edit @@ -462,7 +621,7 @@ holoscan test --local --verbose unset HOLOSCAN_CLI_BUILD_PARENT_DIR ``` -The build goes to `build-native/my_module_pipeline`; CLI `5.0.0` tests with a separate dashboard +The build goes to `build-native/my_module_pipeline`; the CLI tests with a separate dashboard build in `build-/`. Select a specific SDK with `--local-sdk-root /path/to/sdk` or `HOLOSCAN_SDK_ROOT`. Holoscan SDK installs use architecture-qualified layouts such as `install-x86_64` and `install-aarch64`. @@ -475,7 +634,7 @@ cmake --build build -j"$(nproc)" ctest --test-dir build --output-on-failure -L unit ``` -### 4.7 (Optional) Declare dependencies on other Modules +### 4.8 (Optional) Declare dependencies on other Modules Modules become more useful when they compose cleanly. If this Module needs an operator from another Module, declare that relationship in `metadata.json:module.dependencies[]` and pin the @@ -504,11 +663,12 @@ lets a teammate or CI runner resolve the same source later. The [Use a Module tutorial](../use-a-module/README.md) shows how to override that pin with a local checkout during development without weakening the committed dependency. -### 4.8 Build the Debian package +### 4.9 Build the Debian package Now that the operator builds, its behavior is tested, and the demo exits cleanly, we're ready to -package it for another machine. Preview the packaging operation first, just as we did for the -development commands: +package it for another machine. This section applies to Modules with C++ operators; a pure-Python +Module ships a wheel instead (section 4.6). Preview the packaging operation first, just as we did +for the development commands: ```bash holoscan package holoscan-my-module --pkg-generator DEB --dryrun --verbose @@ -537,8 +697,8 @@ PYTHONPATH="$PWD/build/install/lib/python3.12/site-packages:$PYTHONPATH" \ Pass `--prefix` because the default `/usr/local` prefix isn't writable in the development container and isn't on Ubuntu's Python path. Run these commands inside the development container after building the application with Python bindings. A prefix inside `build/` stays out of Git. -Replace `python3.12` if you use a different Python version. Module projects based on Holoscan -SDK 5.0 do not fully support building Python wheels for distribution yet. +Replace `python3.12` if you use a different Python version. Wheel packaging for Modules with +Python bindings is planned for a later release. The final proof is installation in a clean consumer where the Holoscan Debian dependency is available. Extracting the archive may be useful for inspection, but it does not prove that APT can @@ -550,7 +710,7 @@ package database. That alone does not satisfy the generated `Depends: holoscan ( Configure a repository that supplies the required SDK 5 Debian package before testing APT installation, and check its candidate version with `apt-cache policy holoscan`. -### 4.9 Configure shared defaults +### 4.10 Configure shared defaults Most Modules can begin with metadata-derived defaults. When your whole team needs the same CUDA selection, forwarded variable, Docker option, or base image, put that shared choice in @@ -572,7 +732,7 @@ so mount any referenced host path separately. Use exact image references that yo tested, and use `--dryrun --verbose` whenever you need to see which setting came from metadata, project configuration, the environment, or the command line. -### 4.10 Review the generated CI +### 4.11 Review the generated CI The scaffold also gives you a starting CI workflow in `.github/workflows/ci.yml`. It separates fast feedback from hardware-backed validation: @@ -583,20 +743,25 @@ fast feedback from hardware-backed validation: | CMake configure | Holoscan SDK container on a CPU runner | the generated CMake graph without building or using a GPU | | Build and test | self-hosted GPU | the C++ build and CTest suite in the SDK container | +A pure-Python Module's workflow has no C++ formatting or CMake configure job. Instead, it lints +the Python sources and metadata, builds the wheel on a CPU runner, and runs the pytest suite +through CTest in the SDK container on a GPU runner. + Treat this workflow as a useful baseline, not a finished release gate. Update the runner labels and image for your infrastructure, keep the CLI constraints synchronized, and add hardware or end-to-end coverage for the behavior your Module promises. -### 4.11 Publish and share +### 4.12 Publish and share Your Module is now ready to move from a development repository into a release process. Before you share it: 1. Push the canonical repository and set `metadata.json:module.source_repository` to its URL. 2. Replace placeholder authors, license, description, and supported-platform values. -3. Publish the inspected DEB to the APT repository used by your consumers. -4. Make `binary_packages.debian` and its install command match the package users can actually - obtain. +3. Publish the inspected DEB to the APT repository used by your consumers, or, for a pure-Python + Module, publish the wheel to a Python package index such as [PyPI](https://pypi.org/). +4. Make `binary_packages.debian` or `binary_packages.wheel` and the install commands match the + package users can actually obtain. 5. Document the supported SDK versions and the finite example that consumers can run after installation. @@ -614,22 +779,32 @@ Once you are familiar with the workflow, use this table as a quick reminder: | Inspect projects | `holoscan list --json` | | Inspect environment | `holoscan env-info --json` / `holoscan env-check --json` | | Add Python bindings | `holoscan create --language cpp python ...` | +| Create a pure-Python Module | `holoscan create --language python ...` | | Build demo | `holoscan build _pipeline --language cpp` | | Run finite demo | `holoscan run _pipeline --language cpp` | | Run Python demo | `holoscan run _pipeline --language python` | | Run CTest | `holoscan test` | | Package | `holoscan package holoscan- --pkg-generator DEB` | +| Package a pure-Python Module | `holoscan package holoscan- --pkg-generator WHEEL` | +| Use the build tree from Python | `holoscan install --dev --local` | | Install Python package | Build the Python variant, then `cmake --install build/_pipeline --component holoscan--python --prefix ` | | Use an explicit root | `holoscan --project-root /path/to/module ` | ## 6. Troubleshooting -### The CLI rejects `--language python` or WHEEL +### The CLI rejects `--language python` + +Holoscan CLI `5.0.0` doesn't include the pure-Python Module template. Check +`holoscan version` and install CLI `5.1.0` or later. Use a CLI 4.x release for an SDK 4 Module. + +For a Module with C++ operators, use `--language cpp python` to call them from Python, and +install the Python package with `cmake --install` as shown in section 4.9. Wheel packaging for +Python bindings is planned for a later release. + +### pip can't find a `holoscan-core-cu13` wheel -SDK 5.0 Module operators are implemented in C++. Use `--language cpp python` to call them from -Python, and install the Python package with `cmake --install` as shown in section 4.8. -Python-implemented operators and wheels aren't supported by the SDK 5 template yet. Use a CLI 4.x -release for an SDK 4 Module. +The Holoscan SDK 5 wheels need Python 3.12 or later on x86_64 or aarch64 Linux with glibc 2.39 or +newer, for example Ubuntu 24.04. On an older host, use the development container instead. ### `import holoscan` fails after setting `PYTHONPATH` diff --git a/tutorials/holoscan-modules/create-a-module/metadata.json b/tutorials/holoscan-modules/create-a-module/metadata.json index 26c0ac5..63fc7a9 100644 --- a/tutorials/holoscan-modules/create-a-module/metadata.json +++ b/tutorials/holoscan-modules/create-a-module/metadata.json @@ -10,8 +10,9 @@ } ], "language": ["C++", "Python"], - "version": "0.3.1", + "version": "0.4.0", "changelog": { + "0.4.0": "Add the pure-Python Module workflow (--language python) from Holoscan CLI 5.1.0", "0.3.1": "Verify CLI 5.0.0 workflows and clarify image prerequisites, Python build paths, and native CTest behavior", "0.3.0": "Add the optional Python bindings workflow for SDK 5.0 Modules", "0.2.0": "Update the standalone Module workflow for the Holoscan SDK 5.0 C++ API and Debian packaging", From d6296875306653464d52887242041127a0e36e0f Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 6 Nov 2026 21:49:33 +0000 Subject: [PATCH 2/8] docs(holoscan-modules): document wheels for Modules with Python bindings Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tutorials/holoscan-modules/README.md | 5 ++-- .../create-a-module/README.md | 28 +++++++++++++------ .../create-a-module/metadata.json | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/tutorials/holoscan-modules/README.md b/tutorials/holoscan-modules/README.md index 3d998ec..5af5d9a 100644 --- a/tutorials/holoscan-modules/README.md +++ b/tutorials/holoscan-modules/README.md @@ -23,8 +23,9 @@ Browse published modules in the [Modules tab](https://nvidia-holoscan.github.io/ Module operators are implemented in C++ or Python. For C++ operators, the scaffold can add Python bindings so Python applications can use them, and the packaged artifact is a Debian package. -From Holoscan CLI `5.1.0`, `holoscan create --language python` creates a pure-Python Module that -ships as a Python wheel and needs no C++ compiler. +From Holoscan CLI `5.1.0`, the bindings can also ship as a Python wheel, and +`holoscan create --language python` creates a pure-Python Module that ships as a Python wheel and +needs no C++ compiler. The examples install `holoscan-cli` from PyPI; each tutorial pins the CLI version it uses. CLI and SDK versions are independent: the scaffold requires SDK `5.0.0` or later. See the diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 41dd306..75dc4fd 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -33,7 +33,7 @@ its own build, tests, package, and CI. Module operators are implemented in either C++ or Python. For C++ operators, the scaffold can also add Python bindings: a small extension that registers the C++ operators with the SDK -operator registry so Python applications can use them. For Python operators, it creates a +operator registry so Python applications can use them, and that can ship as a Python wheel. For Python operators, it creates a pure-Python Module that needs no C++ code or compiler and ships as a Python wheel. ### Who creates a Holoscan Module? @@ -177,7 +177,7 @@ A Module's operators are either all C++ or all Python: | `--language` | Operators | Use from Python | Package | | --- | --- | --- | --- | | `cpp` | C++ | No | Debian | -| `cpp python` | C++ | Through generated Python bindings | Debian | +| `cpp python` | C++ | Through generated Python bindings | Debian, plus a wheel for the bindings | | `python` | Python | Directly | Python wheel | ### 3.2 Set up the generated development environment @@ -697,8 +697,20 @@ PYTHONPATH="$PWD/build/install/lib/python3.12/site-packages:$PYTHONPATH" \ Pass `--prefix` because the default `/usr/local` prefix isn't writable in the development container and isn't on Ubuntu's Python path. Run these commands inside the development container after building the application with Python bindings. A prefix inside `build/` stays out of Git. -Replace `python3.12` if you use a different Python version. Wheel packaging for Modules with -Python bindings is planned for a later release. +Replace `python3.12` if you use a different Python version. + +To ship the bindings to Python users instead, build them into a wheel: + +```bash +holoscan package holoscan-my-module --pkg-generator WHEEL --dryrun --verbose +holoscan package holoscan-my-module --pkg-generator WHEEL --verbose +``` + +scikit-build-core builds only the operator and its bindings, configured in `pyproject.toml`, and +writes `build/dist/holoscan_my_module-0.1.0-cp312-abi3-linux_x86_64.whl`. The wheel works with +Python 3.12 or later on the architecture it was built on, depends on `holoscan-core-cu13`, and +declares the `holoscan.modules` entry point, so `import holoscan.my_module` works after +`pip install`. The final proof is installation in a clean consumer where the Holoscan Debian dependency is available. Extracting the archive may be useful for inspection, but it does not prove that APT can @@ -759,7 +771,8 @@ share it: 1. Push the canonical repository and set `metadata.json:module.source_repository` to its URL. 2. Replace placeholder authors, license, description, and supported-platform values. 3. Publish the inspected DEB to the APT repository used by your consumers, or, for a pure-Python - Module, publish the wheel to a Python package index such as [PyPI](https://pypi.org/). + Module or Python bindings, publish the wheel to a Python package index such as + [PyPI](https://pypi.org/). 4. Make `binary_packages.debian` or `binary_packages.wheel` and the install commands match the package users can actually obtain. 5. Document the supported SDK versions and the finite example that consumers can run after @@ -785,7 +798,7 @@ Once you are familiar with the workflow, use this table as a quick reminder: | Run Python demo | `holoscan run _pipeline --language python` | | Run CTest | `holoscan test` | | Package | `holoscan package holoscan- --pkg-generator DEB` | -| Package a pure-Python Module | `holoscan package holoscan- --pkg-generator WHEEL` | +| Package a Python wheel | `holoscan package holoscan- --pkg-generator WHEEL` | | Use the build tree from Python | `holoscan install --dev --local` | | Install Python package | Build the Python variant, then `cmake --install build/_pipeline --component holoscan--python --prefix ` | | Use an explicit root | `holoscan --project-root /path/to/module ` | @@ -798,8 +811,7 @@ Holoscan CLI `5.0.0` doesn't include the pure-Python Module template. Check `holoscan version` and install CLI `5.1.0` or later. Use a CLI 4.x release for an SDK 4 Module. For a Module with C++ operators, use `--language cpp python` to call them from Python, and -install the Python package with `cmake --install` as shown in section 4.9. Wheel packaging for -Python bindings is planned for a later release. +install the Python package with `cmake --install` or build a wheel as shown in section 4.9. ### pip can't find a `holoscan-core-cu13` wheel diff --git a/tutorials/holoscan-modules/create-a-module/metadata.json b/tutorials/holoscan-modules/create-a-module/metadata.json index 63fc7a9..2795bc4 100644 --- a/tutorials/holoscan-modules/create-a-module/metadata.json +++ b/tutorials/holoscan-modules/create-a-module/metadata.json @@ -12,7 +12,7 @@ "language": ["C++", "Python"], "version": "0.4.0", "changelog": { - "0.4.0": "Add the pure-Python Module workflow (--language python) from Holoscan CLI 5.1.0", + "0.4.0": "Add the pure-Python Module workflow (--language python) and wheel packaging from Holoscan CLI 5.1.0", "0.3.1": "Verify CLI 5.0.0 workflows and clarify image prerequisites, Python build paths, and native CTest behavior", "0.3.0": "Add the optional Python bindings workflow for SDK 5.0 Modules", "0.2.0": "Update the standalone Module workflow for the Holoscan SDK 5.0 C++ API and Debian packaging", From 6be0057681eb915c6476b1fcc1a4296bd4bd0e66 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 6 Nov 2026 21:57:49 +0000 Subject: [PATCH 3/8] docs(holoscan-modules): show pure-Python operator sources under operators// Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../create-a-module/README.md | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 75dc4fd..48855c1 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -33,8 +33,9 @@ its own build, tests, package, and CI. Module operators are implemented in either C++ or Python. For C++ operators, the scaffold can also add Python bindings: a small extension that registers the C++ operators with the SDK -operator registry so Python applications can use them, and that can ship as a Python wheel. For Python operators, it creates a -pure-Python Module that needs no C++ code or compiler and ships as a Python wheel. +operator registry so Python applications can use them, and that can ship as a Python wheel. For +Python operators, it creates a pure-Python Module that needs no C++ code or compiler and ships as a +Python wheel. ### Who creates a Holoscan Module? @@ -256,8 +257,9 @@ metadata declares a C++ namespace and a Debian binary package; with Python bindi `Python` and the `holoscan_my_module` Python namespace. A Module created with `--language python` has no C++ sources, C++ tests, or Debian package -configuration. Its operators live in the Python package, and its CMake project only stages that -package and runs the tests: +configuration. Its operators live in `operators/`, which the wheel and the build tree install as +the `holoscan_my_module.operators` package. Its CMake project only stages the package and runs the +tests: ```text holoscan-my-module/ @@ -267,12 +269,13 @@ holoscan-my-module/ ├── Dockerfile # SDK 5.0 CUDA 13 development image ├── python/ │ ├── CMakeLists.txt # Stages holoscan_my_module in the build tree -│ └── holoscan_my_module/ -│ └── operators/ -│ └── my_module_op.py # Typed Input/Output class attributes -├── operators/ +│ └── holoscan_my_module/ # Package root (__init__.py, py.typed) +├── operators/ # Installed as holoscan_my_module.operators +│ ├── __init__.py # Exports every operator │ └── my_module_op/ -│ └── metadata.json # Operator metadata only +│ ├── __init__.py +│ ├── my_module_op.py # Typed Input/Output class attributes +│ └── metadata.json # Operator metadata (not packaged) ├── applications/ │ └── my_module_pipeline/ │ └── python/ # Graph → compile → run_async example @@ -492,11 +495,11 @@ For a native Python build, use `build-native/my_module_pipeline/python/lib` inst ### 4.6 Implement operators in pure Python -If you generated the Module with `--language python`, its operators are Python classes in the -`holoscan_my_module` package and there is no C++ to build. They use the same SDK 5 model as the +If you generated the Module with `--language python`, its operators are Python classes under +`operators/` and there is no C++ to build. They use the same SDK 5 model as the C++ operator: typed ports, a `compute()` method, and a graph that you compose, compile, and run. -The generated operator in `python/holoscan_my_module/operators/my_module_op.py` declares its ports +The generated operator in `operators/my_module_op/my_module_op.py` declares its ports as class attributes and forwards each value: ```python @@ -565,8 +568,8 @@ def test_operator_forwards_each_value(): ``` Replace the pass-through assertions with checks for your operator's real transformation. To add -an operator, add a module under `python/holoscan_my_module/operators/`, export it from -`operators/__init__.py`, describe it in `operators//metadata.json` and +an operator, add an `operators//` directory with an `__init__.py`, the implementation, and a +`metadata.json`, export it from `operators/__init__.py`, add it to `metadata.json:module.operator_names`, and cover it in `tests/python`. To use the build tree from a Python shell or notebook, register it with a development hook from From 8222ab1a91ad1f7ecc27df26b8f9270311a54dec Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 6 Nov 2026 22:06:08 +0000 Subject: [PATCH 4/8] docs(holoscan-modules): gate pure-Python operators on OP_ Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../create-a-module/README.md | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 48855c1..5abbac5 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -257,9 +257,10 @@ metadata declares a C++ namespace and a Debian binary package; with Python bindi `Python` and the `holoscan_my_module` Python namespace. A Module created with `--language python` has no C++ sources, C++ tests, or Debian package -configuration. Its operators live in `operators/`, which the wheel and the build tree install as -the `holoscan_my_module.operators` package. Its CMake project only stages the package and runs the -tests: +configuration. Its operators live in `operators/`. As for C++ operators, each one is gated on an +`OP_` CMake option, and the build tree and wheel install the enabled ones as the +`holoscan_my_module.operators` package. The CMake project compiles nothing; it stages the package +and runs the tests: ```text holoscan-my-module/ @@ -271,8 +272,10 @@ holoscan-my-module/ │ ├── CMakeLists.txt # Stages holoscan_my_module in the build tree │ └── holoscan_my_module/ # Package root (__init__.py, py.typed) ├── operators/ # Installed as holoscan_my_module.operators -│ ├── __init__.py # Exports every operator +│ ├── CMakeLists.txt # add_holohub_operator() per operator (OP_) +│ ├── __init__.py # Exports every enabled operator │ └── my_module_op/ +│ ├── CMakeLists.txt # Stages the operator's Python sources │ ├── __init__.py │ ├── my_module_op.py # Typed Input/Output class attributes │ └── metadata.json # Operator metadata (not packaged) @@ -568,9 +571,16 @@ def test_operator_forwards_each_value(): ``` Replace the pass-through assertions with checks for your operator's real transformation. To add -an operator, add an `operators//` directory with an `__init__.py`, the implementation, and a -`metadata.json`, export it from `operators/__init__.py`, add it to -`metadata.json:module.operator_names`, and cover it in `tests/python`. +an operator: + +1. Copy `operators/my_module_op/` to `operators//`: its `__init__.py` exports the class, and + its `CMakeLists.txt` calls `holoscan_my_module_add_python_operator()`. +2. Add `add_holohub_operator()` to `operators/CMakeLists.txt`, and + `cmake.define.OP_ = "ON"` to `[tool.scikit-build]` in `pyproject.toml` so the wheel ships + it. +3. Add it to `metadata.json:module.operator_names` and cover it in `tests/python`. + +`operators/__init__.py` exports every enabled operator, so it needs no change. To use the build tree from a Python shell or notebook, register it with a development hook from the development container (`holoscan run-container`) or a host with the SDK wheels: @@ -595,6 +605,8 @@ python -m pip install -e . pytest python -m pytest tests/python ``` +The editable install copies the operators, so rerun `pip install -e .` after editing them. + When the operators are ready, build the wheel: ```bash From 6a28ecbfeba821c2c6a6ce51c756b1443b298d02 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 6 Nov 2026 22:25:03 +0000 Subject: [PATCH 5/8] docs(holoscan-modules): use a rebuilding editable install for pure-Python Modules Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tutorials/holoscan-modules/create-a-module/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 5abbac5..499f447 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -601,11 +601,14 @@ depends on: ```bash python3 -m venv .venv-module source .venv-module/bin/activate -python -m pip install -e . pytest +python -m pip install "scikit-build-core~=1.0.3" cmake ninja pytest +python -m pip install --no-build-isolation -Ceditable.rebuild=true -e . python -m pytest tests/python ``` -The editable install copies the operators, so rerun `pip install -e .` after editing them. +The editable install rebuilds the enabled operators when the package is imported, so source edits +take effect without reinstalling. It needs `--no-build-isolation` so the rebuild can find CMake +and Ninja. When the operators are ready, build the wheel: From 3eb28230dd45f3283fe90486549dbec160c691dd Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Sun, 8 Nov 2026 08:16:28 +0000 Subject: [PATCH 6/8] docs(holoscan-modules): add type-checking imports for new pure-Python operators Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tutorials/holoscan-modules/create-a-module/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 499f447..9cd98a8 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -578,9 +578,9 @@ an operator: 2. Add `add_holohub_operator()` to `operators/CMakeLists.txt`, and `cmake.define.OP_ = "ON"` to `[tool.scikit-build]` in `pyproject.toml` so the wheel ships it. -3. Add it to `metadata.json:module.operator_names` and cover it in `tests/python`. - -`operators/__init__.py` exports every enabled operator, so it needs no change. +3. Add it to the `if TYPE_CHECKING:` imports in `operators/__init__.py`, so type checkers and IDEs + resolve it. At runtime, `operators/__init__.py` exports every enabled operator. +4. Add it to `metadata.json:module.operator_names` and cover it in `tests/python`. To use the build tree from a Python shell or notebook, register it with a development hook from the development container (`holoscan run-container`) or a host with the SDK wheels: From 6a46077a6b153be186e9849f041aba8737af9014 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 13 Nov 2026 14:44:42 +0000 Subject: [PATCH 7/8] docs(holoscan-modules): match the current Module template's install component, uv group, and namespace Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../create-a-module/README.md | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 9cd98a8..90af2f9 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -188,14 +188,16 @@ Now that the repository exists, let's set up its development tools. The generate host and container commands on the same version. With `uv`: ```bash -uv run holoscan version --json -uv run holoscan list --json +uv run --only-group dev holoscan version --json +uv run --only-group dev holoscan list --json ``` The generated `pyproject.toml` selects NVIDIA's Python index for the CLI; the pip fallback uses the indexes in `requirements-cli.txt`. `pyproject.toml` has `package = false`: uv is provisioning the development environment, not building a Python version -of your Module. +of your Module. `--only-group dev` installs only the development tools. Without it, uv also +installs the runtime dependencies of a Module with Python operators or bindings, including the +`holoscan-core-cu13` wheel, which needs glibc 2.39 or newer. If `uv` is not available, create the same isolated environment with `pip`: @@ -208,7 +210,7 @@ holoscan list --json ``` The parent `.cli-venv` is only for project generation. From the generated Module root, use -`uv run` for later commands, or reactivate that Module's `.venv` if you used pip. +`uv run --only-group dev` for later commands, or reactivate that Module's `.venv` if you used pip. ### 3.3 Inspect the generated tree @@ -254,7 +256,7 @@ Key things to notice: Before publishing, update `metadata.json`, especially `source_repository`, authors, license, operator names, supported platforms, and `holoscan_sdk.minimum_required_version`. The generated metadata declares a C++ namespace and a Debian binary package; with Python bindings it also lists -`Python` and the `holoscan_my_module` Python namespace. +`Python` and the `holoscan.my_module` Python namespace. A Module created with `--language python` has no C++ sources, C++ tests, or Debian package configuration. Its operators live in `operators/`. As for C++ operators, each one is gated on an @@ -458,7 +460,7 @@ Python source and sink operators around the Module operator: ```python import holoscan as hs -from holoscan_my_module.operators import MyModuleOp +from holoscan.my_module.operators import MyModuleOp graph = hs.Graph("my_module_pipeline", default_clock=hs.RealtimeClock) source = graph.op(CounterSource, "source") @@ -526,7 +528,7 @@ source and a counting sink: ```python import holoscan as hs -from holoscan_my_module.operators import MyModuleOp +from holoscan.my_module.operators import MyModuleOp graph = hs.Graph("my_module_pipeline", default_clock=hs.RealtimeClock) source = graph.op(CounterSource, "source") @@ -706,7 +708,7 @@ use it by appending its installation path to your local `PYTHONPATH` environment ```bash # First run the Python variant as shown in section 4.5. -cmake --install build/my_module_pipeline --component holoscan-my-module-python \ +cmake --install build/my_module_pipeline --component holoscan-my_module-python_libs \ --prefix "$PWD/build/install" PYTHONPATH="$PWD/build/install/lib/python3.12/site-packages:$PYTHONPATH" \ python3 -c "import holoscan_my_module; print(holoscan_my_module.__file__)" @@ -818,7 +820,7 @@ Once you are familiar with the workflow, use this table as a quick reminder: | Package | `holoscan package holoscan- --pkg-generator DEB` | | Package a Python wheel | `holoscan package holoscan- --pkg-generator WHEEL` | | Use the build tree from Python | `holoscan install --dev --local` | -| Install Python package | Build the Python variant, then `cmake --install build/_pipeline --component holoscan--python --prefix ` | +| Install Python package | Build the Python variant, then `cmake --install build/_pipeline --component holoscan--python_libs --prefix ` | | Use an explicit root | `holoscan --project-root /path/to/module ` | ## 6. Troubleshooting From 9fbe3f9f5e1e0994ac85cb35ed9b152ff0b3dd40 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 13 Nov 2026 14:47:51 +0000 Subject: [PATCH 8/8] docs(holoscan-modules): separate PyPI advice for pure-Python and bindings wheels Signed-off-by: Tom Birdsong Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tutorials/holoscan-modules/create-a-module/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tutorials/holoscan-modules/create-a-module/README.md b/tutorials/holoscan-modules/create-a-module/README.md index 90af2f9..7264b92 100644 --- a/tutorials/holoscan-modules/create-a-module/README.md +++ b/tutorials/holoscan-modules/create-a-module/README.md @@ -790,9 +790,11 @@ share it: 1. Push the canonical repository and set `metadata.json:module.source_repository` to its URL. 2. Replace placeholder authors, license, description, and supported-platform values. -3. Publish the inspected DEB to the APT repository used by your consumers, or, for a pure-Python - Module or Python bindings, publish the wheel to a Python package index such as - [PyPI](https://pypi.org/). +3. Publish the inspected DEB to the APT repository used by your consumers. Publish a pure-Python + Module's `py3-none-any` wheel to a Python package index such as [PyPI](https://pypi.org/). + The Python bindings wheel is tagged `linux_`, which PyPI rejects. Publish it to an index + that accepts that tag, or build it in a `manylinux` environment and repair it to a `manylinux` + tag first. 4. Make `binary_packages.debian` or `binary_packages.wheel` and the install commands match the package users can actually obtain. 5. Document the supported SDK versions and the finite example that consumers can run after