Files
Aleksandr Motsjonov 667365f45b Unify pybind11 and nanobind into single-source bindings (#5254)
Exploring an idea Anton had about unifiying two python backends into
single source since they are so similar.

## Summary

This PR unifies the early nanobind migration with the existing pybind11
Python bindings by compiling **shared sources** from `src/python/` for
both backends, instead of maintaining parallel copies under
`src/python-nanobind/`.

Migrated modules so far: `ROI`, `TypeDesc`, `ImageSpec`, `ParamValue` /
`ParamValueList`, plus the core `OpenImageIO` module helpers in
`py_oiio.cpp`. Backend differences are isolated in `py_backend.h` (type
aliases, binding API macros like `.OIIO_PY_RW(...)`, and small
`oiio_py::` helpers). Shared Python↔C++ conversion logic lives in
`py_oiio.h` with `#if` only where the backends genuinely differ.

CMake is updated so that when `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind`
is selected, the module is built from the unified sources and installed
as the main `OpenImageIO` package in the expected location. With `both`,
pybind11 remains the primary module and nanobind builds as
`PyOpenImageIONanobind` for side-by-side testing.

Duplicate nanobind-only `.cpp` files are removed (~1.8k lines deleted
net). Testsuite coverage is extended with explicit roundtrip checks for
shared helper paths (attributes, buffers, tuples).

---

## Pros

- **Single source of truth** for migrated bindings — no more keeping two
copies of `py_roi.cpp`, `py_imagespec.cpp`, etc. in sync by hand.
- **Lower risk of backend drift** — a fix or API tweak in a migrated
module automatically applies to both pybind11 and nanobind.
- **Net code reduction** — removes a large duplicated tree under
`src/python-nanobind/` while centralizing shared logic in one place.
- **Clearer migration path** — `MIGRATION_STATUS.md` documents what is
fully migrated, partially migrated, and still pybind-only.
- **Better nanobind-only build layout** — on `main`, choosing nanobind
does not install into the same package layout as pybind11; this PR fixes
that for the migrated surface.
- **Side-by-side validation** — `OIIO_PYTHON_BINDINGS_BACKEND=both`
builds both modules so existing `testsuite/python-*` and `*.nanobind`
variants can be run against the same C++ sources.
- **Explicit parity tests** — new/extended tests cover shared conversion
paths (typed attributes, buffer→vector, tuple roundtrips) rather than
relying on import smoke tests alone.
- **Maintainable binding style** — method-name macros
(`.OIIO_PY_RW(...)`) keep clang-format happy without `clang-format off`
regions.

---

## Cons

- **Added abstraction in the pybind11 path** — even pybind-only builds
now go through `py_backend.h`, macros, and shared helpers; slightly more
indirection than plain `.def_readwrite(...)`.
- **Regression risk for pybind11** — adapting existing pybind code to
work for both backends could subtly change pybind11 behavior at the
edges (init, casting, error paths, ownership).
- **Incomplete migration** — most of the Python API (`ImageBuf`,
`ImageInput`/`ImageOutput`, `ImageBufAlgo`, texture system, etc.) is
still pybind-only; this PR is a foundation, not full nanobind coverage.
- **Known nanobind gaps remain** — module-level exports and helpers
listed in `MIGRATION_STATUS.md` are still missing or intentionally
different vs pybind11.
- **Higher review and test burden** — reviewers need to think about two
backends; CI/local testing should cover both pybind and `.nanobind`
variants for touched code.
- **Conditional compilation complexity** — `#if
defined(OIIO_PY_BACKEND_NANOBIND)` remains in a few places (e.g.
`ParamValue` constructors, buffer dispatch); easy to get wrong if
extended carelessly.
- **Slightly less obvious binding code** — `.OIIO_PY_RW("x", ...)` reads
less directly than `.def_readwrite("x", ...)` until you know the macro
map.

---------

Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
2026-07-09 10:26:04 -07:00
..