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>
First PR on the way to a migration from pybind11 to nanobind.
New env flag introduced `OIIO_PYTHON_BINDINGS_BACKEND` = `pybind11` or
`nanobind` or `both`
> Select which Python binding backend(s) to configure. `both` keeps the
existing pybind11 module and also builds the experimental nanobind
module.
When it is nanobine one is build it is in
`PyOpenImageIONanobindExperimental` target.
```
cmake --fresh -S . -B build \
-DUSE_CCACHE=OFF \
-Dfmt_DIR=/opt/homebrew/lib/cmake/fmt \
-DOpenColorIO_DIR=/opt/homebrew/lib/cmake/OpenColorIO \
-DOIIO_INTERNALIZE_FMT=ON \
-DOIIO_PYTHON_BINDINGS_BACKEND=both
```
### Checklist:
<!-- Put an 'x' in the boxes as you complete the checklist items -->
- [x] **I have read the guidelines** on
[contributions](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/CONTRIBUTING.md)
and [code review
procedures](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/docs/dev/CodeReview.md).
- [x] **I have updated the documentation** if my PR adds features or
changes
behavior.
- [x] **I am sure that this PR's changes are tested somewhere in the
testsuite**.
- [x] **I have run and passed the testsuite in CI** *before* submitting
the
PR, by pushing the changes to my fork and seeing that the automated CI
passed there. (Exceptions: If most tests pass and you can't figure out
why
the remaining ones fail, it's ok to submit the PR and ask for help. Or
if
any failures seem entirely unrelated to your change; sometimes things
break
on the GitHub runners.)
- [x] **My code follows the prevailing code style of this project** and
I
fixed any problems reported by the clang-format CI test.
- [x] If I added or modified a public C++ API call, I have also amended
the
corresponding Python bindings. If altering ImageBufAlgo functions, I
also
exposed the new functionality as oiiotool options.
This code contribution was assisted by Cursor/Composer-2
---------
Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
Add python stub files for code completion and static analysis.
Fixes
https://github.com/AcademySoftwareFoundation/OpenImageIO/issues/4682
No new test cases, since there is not any change to the behavior. I do
have an idea for a kind of test that might validate the stubs against
the python unit tests, but I don't know if it's worth it.
---------
Signed-off-by: Chad Dombrova <chadrik@gmail.com>
These are build-breaking changes that impose a variety of new minimum
dependencies for the toolchain.
The new minimum Python version supported is now 3.7.
Get rid of 'from future' lines in python scripts in testsuite.
Remove python 2.7 specific ref images from testsuite.
Needless to say, this will not be backported to any release branches,
which guarantee that they will never raise the minimum supported
versions of any dependencies. This is just for future OIIO.
Signed-off-by: Larry Gritz <lg@larrygritz.com>
Since LG has re-licensed all his old code under Apache-2.0, we can
change the license notice from BSD to Apache-2.0 for all files in which
LG is the only extant author. Also change all files that LG co-authored
with other people to specify "BSD-3-Clause and Apache-2.0".
Some files (mostly test scripts) had no license notice, so in those
cases, we added one.
Signed-off-by: Larry Gritz <lg@larrygritz.com>
This patch allows ParamValue, ParamValueList, and ImageSpec to support
attribute/getattribute of metadata consisting of uint8[] arrays, which
is how we store certain binary blobs or raw byte arrays. We translate
this to Python as a numpy array of type uint8 (dtype='B').
This fixes a limitation where it was not possible to directly retrieve
or set the "ICCProfile" metadata (which is stored as a uint8 array)
from the Python APIs, as you could easily do from the C++ APIs.
In the process, I found similar bits of code in several different
places, in various states of incompleteness. I refactored to make a
single make_pyobject() utility and then used that call in place of
several other redundant pieces of functionality.
The Python bindings for ParamValueList and ImageSpec allow a "dict"
style (associative array) interface:
imagespec["key"] = value
val = imagespec["key"]
This patch makes some minor improvements:
* Clarify in the docs that like a regular Python dict, the associative
retrieval will raise a KeyError if the key is not found.
* Add `__contains__` python method as a synonym for contains(), thus
implementing `key in dict`.
* Add `__delitem__` python method as a synonym for erase(), thus
implementing `del dict[key]`.
* Add an `ImageSpec.get()` method that is a synonym for getattribute
(which retrieves the item or returns None or a default if not
found), mirroring the behavior of Python's `dict.get()`.
We previously exposed the PV constructors from just a plain int, float,
or string, but there wasn't an easy way to construct from arbitrary data
like there is in C++.
Most oiiotool commands copy metadata to their result from their first
input. Using `--metamerge` causes >2 arity image ops to try to merge
metadata from all of its input images (though earlier arguments have
precedence in the case of name clashes).
Also make IBAprep utility have a bitfield flag for the same purpose,
though it's not the mechanism we use for the oiiotool calls.
Along the way, added `ParamValueList::merge()` merges two PVLs together.
Also fixed a couple typos and updated the oiiotool --help message to point
to the new online docs.
Default was printing different number of digits of precision on
different python versions. So be specific about the format for floats
to get uniform test output on all platforms.
Lots of important classes have "attributes", which are used as option
settings and the like, basically any kind of named/typed data that
objects carry around. The prototypical way we set and retrieve them
is:
obj.attribute (name, typedesc, &data);
found_ok = obj.getattribute (name, typedesc, &data);
This is very general, but it's kind of a wonky syntax. So this patch
introduces an additional synax that is super handy and aesthetically
pleasing:
obj[name] = data; // set, works for nearly any data!
data = obj[name]->get<type>(); // any type
mystring = obj[name]; // turns any data into a string
This magic is accomplished with a helper template called an
AttrDelegate, which we define in attrdelegate.h. This is pretty handy
and we anticipate using it more widely over time, as we add this syntax
to additional classes.
The first class we rig up in this way is ParamValueList, which
necessitates adding attribute() and getattribute() methods to begin
with. It's a natural fit, and you can get a feel for how it looks when
used by examining the new unit tests in paramlist_test.cpp.
Also added to ImageSpec, for which I think this will be a handy new
way to set and retrieve metadata.
There's a bunch more cleanup that should be fairly straightforward to
understand.
Also, we expose two new functions (pieces of which were used internally
before, though we have now made them public and refactored them):
std::string tostring (typedesc, void* data,
const tostring_foratting& fmt={})
Turns any data (described by the typedesc) into a string,
with optional fine-grained control over the formatting.
bool convert_type (TypeDesc srctype, const void* src,
TypeDesc dsttype, void* dst)
Copies with data conversion.