Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,23 @@ macro (oiio_add_all_tests)
endif ()
oiio_tests_pythonpath_env_entry (_nanobind_tests_pythonpath
"${CMAKE_BINARY_DIR}/lib/python/nanobind")
# Keep in sync with the pybind11 python-* tests below as dual-backend
# coverage expands. imageinput/imagebufalgo also need oiio-images.
set (nanobind_python_tests
python-colorconfig
python-deep
python-imagebuf
python-imagecache
python-imageoutput
python-imagespec
python-oiio
python-paramlist
python-roi
python-texturesys
python-typedesc)
set (nanobind_python_tests_imagedir
python-imageinput
python-imagebufalgo)
set (nanobind_python_test_suffix ".nanobind")
if (OIIO_BUILD_PYTHON_PYBIND11)
oiio_add_tests (
Expand Down Expand Up @@ -298,6 +310,12 @@ macro (oiio_add_all_tests)
SUFFIX ${nanobind_python_test_suffix}
ENVIRONMENT "${_nanobind_tests_pythonpath}"
)
oiio_add_tests (
${nanobind_python_tests_imagedir}
SUFFIX ${nanobind_python_test_suffix}
IMAGEDIR oiio-images
ENVIRONMENT "${_nanobind_tests_pythonpath}"
)
endif ()
endif ()

Expand Down
12 changes: 10 additions & 2 deletions src/python-nanobind/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@

set (nanobind_srcs
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_oiio.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_roi.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagespec.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_deepdata.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_colorconfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageinput.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageoutput.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebuf.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagecache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_texturesys.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebufalgo.cpp)

set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO)
file (MAKE_DIRECTORY ${nanobind_build_package_dir})
Expand Down
10 changes: 9 additions & 1 deletion src/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ set (python_dual_backend_srcs
py_roi.cpp
py_typedesc.cpp
py_imagespec.cpp
py_paramvalue.cpp)
py_paramvalue.cpp
py_deepdata.cpp
py_colorconfig.cpp
py_imageinput.cpp
py_imageoutput.cpp
py_imagebuf.cpp
py_imagecache.cpp
py_texturesys.cpp
py_imagebufalgo.cpp)

# Full pybind11 module (pybind11 or both backends).
if (OIIO_BUILD_PYTHON_PYBIND11)
Expand Down
73 changes: 38 additions & 35 deletions src/python/MIGRATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,50 @@ Nanobind shares binding sources with pybind11 under `src/python/` (see
with `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind` for nanobind-only (`PyOpenImageIO`
/ module `OpenImageIO` in site-packages), or `both` to also build
`PyOpenImageIONanobind` (`_OpenImageIO` under `lib/python/nanobind/OpenImageIO`).
Nanobind-only code paths live in `py_oiio.h` / `py_oiio.cpp` behind
Nanobind-only code paths live in binding `.cpp` files and `py_oiio.cpp` behind
`OIIO_PY_BACKEND_NANOBIND`. Shared Python↔C++ conversion helpers live in
`py_oiio.h` for both backends.
`py_oiio.h` / `py_backend.h` for both backends.

## Migrated — full parity with pybind (no known gaps for this surface)
## Migrated — full dual-backend sources

| Source file | Python / C++ API |
| --- | --- |
| `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> |
| `py_typedesc.cpp` | <code>TypeDesc</code><br>Enums: <code>BASETYPE</code>, <code>AGGREGATE</code>, <code>VECSEMANTICS</code><br>Module <code>Type*</code> constants |
| `py_imagespec.cpp` | <code>ImageSpec</code> (bound methods/properties, typed <code>attribute</code> / buffer paths via shared helpers). |
| `py_paramvalue.cpp` | <code>ParamValue</code>, <code>ParamValueList</code><br>Enum: <code>Interp</code> |

## Migrated — partial (gaps or intentional deltas vs pybind)

| Source file | Migrated (vs pybind) | Missing or divergent (vs pybind) |
| --- | --- | --- |
| `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> |
| `__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. |

---

## Not migrated — entire pybind modules

These exist only in the pybind11 module today (not in `python_dual_backend_srcs`).
All modules below compile for both pybind11 and nanobind from `src/python/`:

| Source file | Python / C++ API |
| --- | --- |
| `py_imageinput.cpp` | <ul><li><code>ImageInput</code></li><li>open, read, formats, …</li></ul> |
| `py_imageoutput.cpp` | <code>ImageOutput</code> |
| `py_imagebuf.cpp` | <code>ImageBuf</code> |
| `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> |
| `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> |
| `py_imagecache.cpp` | <code>ImageCache</code> (wrapped) |
| `py_colorconfig.cpp` | <code>ColorConfig</code> |
| `py_deepdata.cpp` | <code>DeepData</code> |

---
| `py_roi.cpp` | `ROI`, free functions (`union`, `intersection`, `get_roi`, …) |
| `py_typedesc.cpp` | `TypeDesc`, enums, module `Type*` constants |
| `py_imagespec.cpp` | `ImageSpec` |
| `py_paramvalue.cpp` | `ParamValue`, `ParamValueList`, `Interp` |
| `py_deepdata.cpp` | `DeepData` |
| `py_colorconfig.cpp` | `ColorConfig`, module color constants |
| `py_imageinput.cpp` | `ImageInput` |
| `py_imageoutput.cpp` | `ImageOutput` |
| `py_imagebuf.cpp` | `ImageBuf` |
| `py_imagecache.cpp` | `ImageCache` (wrapped) |
| `py_texturesys.cpp` | `Wrap`, `MipMode`, `InterpMode`, `TextureOpt`, `TextureSystem` |
| `py_imagebufalgo.cpp` | `ImageBufAlgo`, `PixelStats`, `CompareResults`, `IBA_*` |
| `py_oiio.cpp` | Module-level attributes and global helpers |

## Consumer-visible differences (pybind11 vs nanobind)

**None intended.** Callers of the pybind11 module should see the same Python API
and behavior with the nanobind build. Binding-side `#if` / caster / constructor
differences exist only to keep that parity.

If you find a behavioral difference, treat it as a bug and add a regression test.

## Packaging / install layout

| Item | Notes |
| --- | --- |
| `__init__.py` | Shared env setup; CLI entry-point trampolines still TODO for full wheel layout. |
| `both` layout | With `OIIO_PYTHON_BINDINGS_BACKEND=both`, nanobind installs under `lib/python/nanobind/` via `src/python-nanobind/` (`_OpenImageIO` + package `__init__.py`). Default pybind11 install path is unchanged. |

## Conventions
## Conventions (maintainers)

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

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.
Extend **testsuite** coverage when adding behavior; run both pybind and `*.nanobind` ctest variants when `OIIO_PYTHON_BINDINGS_BACKEND=both`.
20 changes: 18 additions & 2 deletions src/python/py_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@
# include <nanobind/nanobind.h>
# include <nanobind/ndarray.h>
# include <nanobind/operators.h>
# include <nanobind/stl/array.h>
# include <nanobind/stl/optional.h>
# include <nanobind/stl/string.h>
# include <nanobind/stl/string_view.h>
# include <nanobind/stl/unique_ptr.h>
# include <nanobind/stl/vector.h>

namespace py = nanobind;
using py_module = nanobind::module_;
using namespace py::literals;

# define OIIO_PY_RW def_rw
# define OIIO_PY_RO def_ro
# define OIIO_PY_PROP_RO def_prop_ro
# define OIIO_PY_PROP_RW def_prop_rw
// Allow assigning None to a property (nanobind rejects it unless annotated).
# define OIIO_PY_PROP_RW_NONE(name, getter, setter) \
def_prop_rw(name, getter, setter, py::for_setter(py::arg().none()))
# define OIIO_PY_RO_STATIC def_prop_ro_static
# define OIIO_PY_RO def_ro

namespace oiio_py {

Expand All @@ -41,13 +50,16 @@ make_tuple(size_t size, F&& fill)
return py::steal<py::tuple>(PyList_AsTuple(list.ptr()));
}

inline constexpr auto ref = py::rv_policy::reference;
inline constexpr auto ref_internal = py::rv_policy::reference_internal;

// Copy into std::string so temporary string_view contents stay valid;
// nanobind's string caster turns this into a Python str.
template<typename T>
inline auto
inline std::string
str(T&& x)
{
return std::forward<T>(x);
return std::string(std::forward<T>(x));
}

inline void
Expand Down Expand Up @@ -102,9 +114,12 @@ using py_module = pybind11::module;
using namespace py::literals;

# define OIIO_PY_RW def_readwrite
# define OIIO_PY_RO def_readonly
# define OIIO_PY_PROP_RO def_property_readonly
# define OIIO_PY_PROP_RW def_property
# define OIIO_PY_PROP_RW_NONE def_property
# define OIIO_PY_RO_STATIC def_property_readonly_static
# define OIIO_PY_RO def_readonly

namespace oiio_py {

Expand All @@ -125,6 +140,7 @@ make_tuple(size_t size, F&& fill)
return result;
}

inline constexpr auto ref = py::return_value_policy::reference;
inline constexpr auto ref_internal = py::return_value_policy::reference_internal;

template<typename T>
Expand Down
13 changes: 7 additions & 6 deletions src/python/py_colorconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ namespace PyOpenImageIO {

// Declare the OIIO ColorConfig class to Python
void
declare_colorconfig(py::module& m)
declare_colorconfig(py_module& m)
{
using namespace pybind11::literals;

py::class_<ColorConfig>(m, "ColorConfig")

.def(py::init<>())
Expand Down Expand Up @@ -192,9 +190,12 @@ declare_colorconfig(py::module& m)
return std::nullopt;
})
.def("configname", &ColorConfig::configname)
.def_static("default_colorconfig", []() -> const ColorConfig& {
return ColorConfig::default_colorconfig();
});
.def_static(
"default_colorconfig",
[]() -> const ColorConfig& {
return ColorConfig::default_colorconfig();
},
oiio_py::ref);

m.attr("supportsOpenColorIO") = ColorConfig::supportsOpenColorIO();
m.attr("OpenColorIO_version_hex") = ColorConfig::OpenColorIO_version_hex();
Expand Down
37 changes: 16 additions & 21 deletions src/python/py_deepdata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,24 @@ DeepData_merge_deep_pixels(DeepData& dd, int64_t pixel, const DeepData& src,

// Declare the OIIO DeepData class to Python
void
declare_deepdata(py::module& m)
declare_deepdata(py_module& m)
{
using namespace pybind11::literals;

py::class_<DeepData>(m, "DeepData")
.def_property_readonly("pixels",
[](const DeepData& d) { return d.pixels(); })
.def_property_readonly("channels",
[](const DeepData& d) { return d.channels(); })
.def_property_readonly("A_channel",
[](const DeepData& d) { return d.A_channel(); })
.def_property_readonly("AR_channel",
[](const DeepData& d) { return d.AR_channel(); })
.def_property_readonly("AG_channel",
[](const DeepData& d) { return d.AG_channel(); })
.def_property_readonly("AB_channel",
[](const DeepData& d) { return d.AB_channel(); })
.def_property_readonly("Z_channel",
[](const DeepData& d) { return d.Z_channel(); })
.def_property_readonly("Zback_channel",
[](const DeepData& d) {
return d.Zback_channel();
})
.OIIO_PY_PROP_RO("pixels", [](const DeepData& d) { return d.pixels(); })
.OIIO_PY_PROP_RO("channels",
[](const DeepData& d) { return d.channels(); })
.OIIO_PY_PROP_RO("A_channel",
[](const DeepData& d) { return d.A_channel(); })
.OIIO_PY_PROP_RO("AR_channel",
[](const DeepData& d) { return d.AR_channel(); })
.OIIO_PY_PROP_RO("AG_channel",
[](const DeepData& d) { return d.AG_channel(); })
.OIIO_PY_PROP_RO("AB_channel",
[](const DeepData& d) { return d.AB_channel(); })
.OIIO_PY_PROP_RO("Z_channel",
[](const DeepData& d) { return d.Z_channel(); })
.OIIO_PY_PROP_RO("Zback_channel",
[](const DeepData& d) { return d.Zback_channel(); })

.def(py::init<>())
.def("init", &DeepData_init, "npixels"_a, "nchannels"_a,
Expand Down
Loading
Loading