Skip to content

Commit 79a4233

Browse files
committed
feat(python): Expand dual-backend support in Python bindings
Enhanced the Python bindings to support both pybind11 and nanobind backends by adding new source files to the CMake configuration. Updated the MIGRATION_STATUS.md to reflect the current state of dual-backend integration, detailing the modules that have been migrated and their functionalities. Refactored binding declarations to ensure consistency across both backends, improving the overall structure and maintainability of the code. Assisted-by: Cursor / Auto Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
1 parent a78db1c commit 79a4233

14 files changed

Lines changed: 592 additions & 399 deletions

src/python-nanobind/CMakeLists.txt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44

55
set (nanobind_srcs
66
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_oiio.cpp
7-
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp
87
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_roi.cpp
8+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp
99
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagespec.cpp
10-
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp)
10+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp
11+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_deepdata.cpp
12+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_colorconfig.cpp
13+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageinput.cpp
14+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageoutput.cpp
15+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebuf.cpp
16+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagecache.cpp
17+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_texturesys.cpp
18+
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebufalgo.cpp)
1119

1220
set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO)
1321
file (MAKE_DIRECTORY ${nanobind_build_package_dir})

src/python/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ set (python_dual_backend_srcs
88
py_roi.cpp
99
py_typedesc.cpp
1010
py_imagespec.cpp
11-
py_paramvalue.cpp)
11+
py_paramvalue.cpp
12+
py_deepdata.cpp
13+
py_colorconfig.cpp
14+
py_imageinput.cpp
15+
py_imageoutput.cpp
16+
py_imagebuf.cpp
17+
py_imagecache.cpp
18+
py_texturesys.cpp
19+
py_imagebufalgo.cpp)
1220

1321
# Full pybind11 module (pybind11 or both backends).
1422
if (OIIO_BUILD_PYTHON_PYBIND11)

src/python/MIGRATION_STATUS.md

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,51 @@ Nanobind shares binding sources with pybind11 under `src/python/` (see
55
with `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind` for nanobind-only (`PyOpenImageIO`
66
/ module `OpenImageIO` in site-packages), or `both` to also build
77
`PyOpenImageIONanobind` (`_OpenImageIO` under `lib/python/nanobind/OpenImageIO`).
8-
Nanobind-only code paths live in `py_oiio.h` / `py_oiio.cpp` behind
8+
Nanobind-only code paths live in binding `.cpp` files and `py_oiio.cpp` behind
99
`OIIO_PY_BACKEND_NANOBIND`. Shared Python↔C++ conversion helpers live in
1010
`py_oiio.h` for both backends.
1111

12-
## Migrated — full parity with pybind (no known gaps for this surface)
12+
## Migrated — full dual-backend sources
13+
14+
All modules below compile for both pybind11 and nanobind from `src/python/`:
1315

1416
| Source file | Python / C++ API |
1517
| --- | --- |
16-
| `py_roi.cpp` | <code>ROI</code><br>Free functions:<br><ul><li><code>union</code></li><li><code>intersection</code></li><li><code>get_roi</code></li><li><code>get_roi_full</code></li><li><code>set_roi</code></li><li><code>set_roi_full</code></li></ul> |
17-
| `py_typedesc.cpp` | <code>TypeDesc</code><br>Enums: <code>BASETYPE</code>, <code>AGGREGATE</code>, <code>VECSEMANTICS</code><br>Module <code>Type*</code> constants |
18-
| `py_imagespec.cpp` | <code>ImageSpec</code> (bound methods/properties, typed <code>attribute</code> / buffer paths via shared helpers). |
19-
| `py_paramvalue.cpp` | <code>ParamValue</code>, <code>ParamValueList</code><br>Enum: <code>Interp</code> |
20-
21-
## Migrated — partial (gaps or intentional deltas vs pybind)
22-
23-
| Source file | Migrated (vs pybind) | Missing or divergent (vs pybind) |
24-
| --- | --- | --- |
25-
| `py_oiio.cpp` (`OpenImageIO` module) | <ul><li><code>attribute</code> (one-arg and typed)</li><li><code>get_int_attribute</code></li><li><code>get_float_attribute</code></li><li><code>get_string_attribute</code></li><li><code>getattribute</code></li><li><code>__version__</code>, <code>VERSION_STRING</code></li></ul> | <ul><li><code>geterror</code></li><li><code>get_bytes_attribute</code></li><li>Module <code>set_colorspace</code> (helper taking <code>ImageSpec</code> — the instance method is on <code>ImageSpec</code> in nanobind)</li><li><code>set_colorspace_rec709_gamma</code></li><li><code>equivalent_colorspace</code></li><li><code>is_imageio_format_name</code></li><li><code>AutoStride</code></li><li><code>openimageio_version</code>, <code>VERSION</code>, <code>VERSION_MAJOR</code>, <code>VERSION_MINOR</code>, <code>VERSION_PATCH</code>, <code>INTRO_STRING</code></li><li>Optional: stack traces when <code>OPENIMAGEIO_DEBUG_PYTHON</code> is set (<code>Sysutil</code>)</li><li><code>make_pyobject</code>: no pybind-style <code>debugfmt</code> when the type is unhandled (returns default quietly)</li></ul> |
26-
| `__init__.py` (package) | Shared with pybind11: env / DLL path setup, <code>from .OpenImageIO import *</code>. | <strong>TODO:</strong> Python CLI entry-point trampolines when the install layout matches the full wheel. |
27-
28-
---
29-
30-
## Not migrated — entire pybind modules
18+
| `py_roi.cpp` | `ROI`, free functions (`union`, `intersection`, `get_roi`, …) |
19+
| `py_typedesc.cpp` | `TypeDesc`, enums, module `Type*` constants |
20+
| `py_imagespec.cpp` | `ImageSpec` |
21+
| `py_paramvalue.cpp` | `ParamValue`, `ParamValueList`, `Interp` |
22+
| `py_deepdata.cpp` | `DeepData` |
23+
| `py_colorconfig.cpp` | `ColorConfig`, module color constants |
24+
| `py_imageinput.cpp` | `ImageInput` |
25+
| `py_imageoutput.cpp` | `ImageOutput` |
26+
| `py_imagebuf.cpp` | `ImageBuf` |
27+
| `py_imagecache.cpp` | `ImageCache` (wrapped) |
28+
| `py_texturesys.cpp` | `Wrap`, `MipMode`, `InterpMode`, `TextureOpt`, `TextureSystem` |
29+
| `py_imagebufalgo.cpp` | `ImageBufAlgo`, `PixelStats`, `CompareResults`, `IBA_*` |
30+
| `py_oiio.cpp` | Module-level attributes and global helpers |
31+
32+
## Known nanobind deltas (vs pybind11)
33+
34+
| Area | Notes |
35+
| --- | --- |
36+
| `half` numpy arrays | Multi-dimensional reads return **float32** numpy (nanobind has no native `half` ndarray). pybind11 returns float16. |
37+
| `ImageBuf` constructors | Lambda/`buffer` inits use nanobind `__init__` placement-new (same Python API). |
38+
| `ParamValue` constructors | Same placement-new pattern as before. |
39+
| Enum `.export_values()` | `py_typedesc.cpp` and `py_imagebufalgo.cpp` set module attrs explicitly on nanobind. |
40+
| `py_typedesc.cpp` | `py::implicitly_convertible` — verify if needed on nanobind (may be pybind-only). |
3141

32-
These exist only in the pybind11 module today (not in `python_dual_backend_srcs`).
42+
## Not migrated / packaging
3343

34-
| Source file | Python / C++ API |
44+
| Item | Notes |
3545
| --- | --- |
36-
| `py_imageinput.cpp` | <ul><li><code>ImageInput</code></li><li>open, read, formats, …</li></ul> |
37-
| `py_imageoutput.cpp` | <code>ImageOutput</code> |
38-
| `py_imagebuf.cpp` | <code>ImageBuf</code> |
39-
| `py_imagebufalgo.cpp` | <ul><li><code>ImageBufAlgo</code> (namespace)</li><li><code>PixelStats</code></li><li><code>CompareResults</code></li><li>Exposed <code>IBA_*</code> helpers</li></ul> |
40-
| `py_texturesys.cpp` | <ul><li><code>Wrap</code></li><li><code>MipMode</code></li><li><code>InterpMode</code></li><li><code>TextureOpt</code></li><li><code>TextureSystem</code></li></ul> |
41-
| `py_imagecache.cpp` | <code>ImageCache</code> (wrapped) |
42-
| `py_colorconfig.cpp` | <code>ColorConfig</code> |
43-
| `py_deepdata.cpp` | <code>DeepData</code> |
44-
45-
---
46+
| `__init__.py` | Shared env setup; CLI entry-point trampolines still TODO for full wheel layout. |
4647

4748
## Conventions
4849

49-
When adding coverage, prefer mirroring the existing `declare_*` split in `src/python/` unless a file becomes too large.
50+
- Binding macros: `.OIIO_PY_RW`, `.OIIO_PY_PROP_RO`, `.OIIO_PY_PROP_RW`, `.OIIO_PY_RO`, `.OIIO_PY_RO_STATIC` (see `py_backend.h`).
51+
- Declare functions use `py_module&`, not `py::module&`.
52+
- Buffer I/O: `oiio_py_request_buffer()` / `oiio_bufinfo_from_object()` (both backends).
53+
- `#if defined(OIIO_PY_BACKEND_NANOBIND)` only where backends genuinely differ.
5054

51-
Extend **testsuite** coverage for any migrated code that is not already covered, so **parity with pybind11** is demonstrated rather than only claimed. Follow the existing `testsuite/python-*` scripts and `ref/out.txt` pattern where applicable.
55+
Extend **testsuite** coverage when adding behavior; run both pybind and `*.nanobind` ctest variants when `OIIO_PYTHON_BINDINGS_BACKEND=both`.

src/python/py_backend.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# include <nanobind/nanobind.h>
1111
# include <nanobind/ndarray.h>
1212
# include <nanobind/operators.h>
13+
# include <nanobind/stl/array.h>
14+
# include <nanobind/stl/optional.h>
1315
# include <nanobind/stl/string.h>
1416
# include <nanobind/stl/vector.h>
1517

@@ -21,6 +23,7 @@ using namespace py::literals;
2123
# define OIIO_PY_PROP_RO def_prop_ro
2224
# define OIIO_PY_PROP_RW def_prop_rw
2325
# define OIIO_PY_RO_STATIC def_prop_ro_static
26+
# define OIIO_PY_RO def_ro
2427

2528
namespace oiio_py {
2629

@@ -105,6 +108,7 @@ using namespace py::literals;
105108
# define OIIO_PY_PROP_RO def_property_readonly
106109
# define OIIO_PY_PROP_RW def_property
107110
# define OIIO_PY_RO_STATIC def_property_readonly_static
111+
# define OIIO_PY_RO def_readonly
108112

109113
namespace oiio_py {
110114

src/python/py_colorconfig.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ namespace PyOpenImageIO {
1212

1313
// Declare the OIIO ColorConfig class to Python
1414
void
15-
declare_colorconfig(py::module& m)
15+
declare_colorconfig(py_module& m)
1616
{
17-
using namespace pybind11::literals;
18-
1917
py::class_<ColorConfig>(m, "ColorConfig")
2018

2119
.def(py::init<>())
@@ -192,9 +190,12 @@ declare_colorconfig(py::module& m)
192190
return std::nullopt;
193191
})
194192
.def("configname", &ColorConfig::configname)
195-
.def_static("default_colorconfig", []() -> const ColorConfig& {
196-
return ColorConfig::default_colorconfig();
197-
});
193+
.def_static(
194+
"default_colorconfig",
195+
[]() -> const ColorConfig& {
196+
return ColorConfig::default_colorconfig();
197+
},
198+
oiio_py::ref_internal);
198199

199200
m.attr("supportsOpenColorIO") = ColorConfig::supportsOpenColorIO();
200201
m.attr("OpenColorIO_version_hex") = ColorConfig::OpenColorIO_version_hex();

src/python/py_deepdata.cpp

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,29 +70,24 @@ DeepData_merge_deep_pixels(DeepData& dd, int64_t pixel, const DeepData& src,
7070

7171
// Declare the OIIO DeepData class to Python
7272
void
73-
declare_deepdata(py::module& m)
73+
declare_deepdata(py_module& m)
7474
{
75-
using namespace pybind11::literals;
76-
7775
py::class_<DeepData>(m, "DeepData")
78-
.def_property_readonly("pixels",
79-
[](const DeepData& d) { return d.pixels(); })
80-
.def_property_readonly("channels",
81-
[](const DeepData& d) { return d.channels(); })
82-
.def_property_readonly("A_channel",
83-
[](const DeepData& d) { return d.A_channel(); })
84-
.def_property_readonly("AR_channel",
85-
[](const DeepData& d) { return d.AR_channel(); })
86-
.def_property_readonly("AG_channel",
87-
[](const DeepData& d) { return d.AG_channel(); })
88-
.def_property_readonly("AB_channel",
89-
[](const DeepData& d) { return d.AB_channel(); })
90-
.def_property_readonly("Z_channel",
91-
[](const DeepData& d) { return d.Z_channel(); })
92-
.def_property_readonly("Zback_channel",
93-
[](const DeepData& d) {
94-
return d.Zback_channel();
95-
})
76+
.OIIO_PY_PROP_RO("pixels", [](const DeepData& d) { return d.pixels(); })
77+
.OIIO_PY_PROP_RO("channels",
78+
[](const DeepData& d) { return d.channels(); })
79+
.OIIO_PY_PROP_RO("A_channel",
80+
[](const DeepData& d) { return d.A_channel(); })
81+
.OIIO_PY_PROP_RO("AR_channel",
82+
[](const DeepData& d) { return d.AR_channel(); })
83+
.OIIO_PY_PROP_RO("AG_channel",
84+
[](const DeepData& d) { return d.AG_channel(); })
85+
.OIIO_PY_PROP_RO("AB_channel",
86+
[](const DeepData& d) { return d.AB_channel(); })
87+
.OIIO_PY_PROP_RO("Z_channel",
88+
[](const DeepData& d) { return d.Z_channel(); })
89+
.OIIO_PY_PROP_RO("Zback_channel",
90+
[](const DeepData& d) { return d.Zback_channel(); })
9691

9792
.def(py::init<>())
9893
.def("init", &DeepData_init, "npixels"_a, "nchannels"_a,

0 commit comments

Comments
 (0)