From de73b0eada89889b513833cd8fc2ac06d2f3779a Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 09:39:16 +1100 Subject: [PATCH 01/26] Initial nanobind experimental Signed-off-by: Aleksandr Motsjonov --- CMakeLists.txt | 7 +- INSTALL.md | 8 ++ src/cmake/externalpackages.cmake | 41 +++++- src/cmake/pythonutils.cmake | 118 +++++++++++++++++- src/cmake/testing.cmake | 3 + src/include/CMakeLists.txt | 10 ++ src/python-nanobind/CMakeLists.txt | 21 ++++ src/python-nanobind/__init__.py | 44 +++++++ src/python-nanobind/nanobind_experimental.cpp | 74 +++++++++++ src/python-nanobind/nanobind_experimental.py | 5 + .../python-nanobind-experimental/ref/out.txt | 10 ++ testsuite/python-nanobind-experimental/run.py | 7 ++ .../src/test_nanobind_experimental.py | 60 +++++++++ 13 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 src/python-nanobind/CMakeLists.txt create mode 100644 src/python-nanobind/__init__.py create mode 100644 src/python-nanobind/nanobind_experimental.cpp create mode 100644 src/python-nanobind/nanobind_experimental.py create mode 100644 testsuite/python-nanobind-experimental/ref/out.txt create mode 100644 testsuite/python-nanobind-experimental/run.py create mode 100644 testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f39c20197..5679f1942c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,7 +308,12 @@ else () set (_py_dev_found Python3_Development.Module_FOUND) endif () if (USE_PYTHON AND ${_py_dev_found} AND NOT BUILD_OIIOUTIL_ONLY) - add_subdirectory (src/python) + if (OIIO_BUILD_PYTHON_PYBIND11) + add_subdirectory (src/python) + endif () + if (OIIO_BUILD_PYTHON_NANOBIND) + add_subdirectory (src/python-nanobind) + endif () else () message (STATUS "Not building Python bindings: USE_PYTHON=${USE_PYTHON}, Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}") endif () diff --git a/INSTALL.md b/INSTALL.md index 32582750f9..4b8e5867be 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -42,6 +42,9 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**. * Python >= 3.9 (tested through 3.13). * pybind11 >= 2.7 (tested through 3.0) * NumPy (tested through 2.2.4) + * For the experimental nanobind migration backend: + * nanobind discoverable by CMake, or installed in the active Python + environment so `python -m nanobind --cmake_dir` works * If you want support for PNG files: * libPNG >= 1.6.0 (tested though 1.6.50) * If you want support for camera "RAW" formats: @@ -157,6 +160,10 @@ Make wrapper (`make PkgName_ROOT=...`). `USE_PYTHON=0` : Omits building the Python bindings. +`OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python +binding backend(s) to configure. `both` keeps the existing pybind11 module and +also builds the experimental nanobind module. + `OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them unless you are a developer of OIIO or want to verify that your build passes all tests). @@ -247,6 +254,7 @@ Additionally, a few helpful modifiers alter some build-time options: | make USE_QT=0 ... | Skip anything that needs Qt | | make MYCC=xx MYCXX=yy ... | Use custom compilers | | make USE_PYTHON=0 ... | Don't build the Python binding | +| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | Build the existing pybind11 bindings and the experimental nanobind module | | make BUILD_SHARED_LIBS=0 | Build static library instead of shared | | make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies | | make LINKSTATIC=1 ... | Link with static external libraries when possible | diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index cb366b2a55..dfa7cfd85f 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -118,9 +118,13 @@ endif() if (USE_PYTHON) find_python() endif () -if (USE_PYTHON) +if (USE_PYTHON AND OIIO_BUILD_PYTHON_PYBIND11) checked_find_package (pybind11 REQUIRED VERSION_MIN 2.7) endif () +if (USE_PYTHON AND OIIO_BUILD_PYTHON_NANOBIND) + discover_nanobind_cmake_dir() + checked_find_package (nanobind CONFIG REQUIRED) +endif () ########################################################################### @@ -238,7 +242,40 @@ checked_find_package (fmt REQUIRED VERSION_MIN 9.0 BUILD_LOCAL missing ) -get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only INTERFACE_INCLUDE_DIRECTORIES) +unset (FMT_INCLUDE_DIR) +if (TARGET fmt::fmt-header-only) + get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only + INTERFACE_INCLUDE_DIRECTORIES) +endif () +if ((NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") + AND TARGET fmt::fmt) + get_target_property(FMT_INCLUDE_DIR fmt::fmt + INTERFACE_INCLUDE_DIRECTORIES) +endif () +if (FMT_INCLUDE_DIR AND ";" IN_LIST FMT_INCLUDE_DIR) + list (GET FMT_INCLUDE_DIR 0 FMT_INCLUDE_DIR) +endif () +foreach (_fmt_include_var fmt_INCLUDE_DIRS fmt_INCLUDE_DIR FMT_INCLUDE_DIRS FMT_INCLUDE_DIR) + if (NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") + if (DEFINED ${_fmt_include_var} AND NOT "${${_fmt_include_var}}" STREQUAL "") + set (FMT_INCLUDE_DIR "${${_fmt_include_var}}") + endif () + endif () +endforeach () +if (FMT_INCLUDE_DIR AND ";" IN_LIST FMT_INCLUDE_DIR) + list (GET FMT_INCLUDE_DIR 0 FMT_INCLUDE_DIR) +endif () +if ((NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") + AND fmt_DIR) + get_filename_component (_fmt_prefix "${fmt_DIR}" DIRECTORY) + get_filename_component (_fmt_prefix "${_fmt_prefix}" DIRECTORY) + get_filename_component (_fmt_prefix "${_fmt_prefix}" DIRECTORY) + if (EXISTS "${_fmt_prefix}/include/fmt") + set (FMT_INCLUDE_DIR "${_fmt_prefix}/include") + endif () +endif () +unset (_fmt_include_var) +unset (_fmt_prefix) ########################################################################### diff --git a/src/cmake/pythonutils.cmake b/src/cmake/pythonutils.cmake index efab6ea63d..ae63bc9385 100644 --- a/src/cmake/pythonutils.cmake +++ b/src/cmake/pythonutils.cmake @@ -8,6 +8,25 @@ set (PYTHON_VERSION "" CACHE STRING "Target version of python to find") option (PYLIB_INCLUDE_SONAME "If ON, soname/soversion will be set for Python module library" OFF) option (PYLIB_LIB_PREFIX "If ON, prefix the Python module with 'lib'" OFF) set (PYMODULE_SUFFIX "" CACHE STRING "Suffix to add to Python module init namespace") +set (OIIO_PYTHON_BINDINGS_BACKEND "pybind11" CACHE STRING + "Which Python binding backend(s) to build: pybind11, nanobind, or both") +set_property (CACHE OIIO_PYTHON_BINDINGS_BACKEND PROPERTY STRINGS + pybind11 nanobind both) +string (TOLOWER "${OIIO_PYTHON_BINDINGS_BACKEND}" OIIO_PYTHON_BINDINGS_BACKEND) +if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind|both)$") + message (FATAL_ERROR + "OIIO_PYTHON_BINDINGS_BACKEND must be one of: pybind11, nanobind, both") +endif () +set (OIIO_BUILD_PYTHON_PYBIND11 OFF) +set (OIIO_BUILD_PYTHON_NANOBIND OFF) +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11" + OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both") + set (OIIO_BUILD_PYTHON_PYBIND11 ON) +endif () +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind" + OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both") + set (OIIO_BUILD_PYTHON_NANOBIND ON) +endif () if (WIN32) set (PYLIB_LIB_TYPE SHARED CACHE STRING "Type of library to build for python module (MODULE or SHARED)") else () @@ -54,6 +73,15 @@ macro (find_python) Python3_Development.Module_FOUND Python3_Interpreter_FOUND ) + if (OIIO_BUILD_PYTHON_NANOBIND) + # nanobind's CMake package expects the generic FindPython targets and + # variables (Python::Module, Python_EXECUTABLE, etc.), not the + # versioned Python3::* targets that the rest of OIIO uses today. + find_package (Python ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR} + EXACT REQUIRED + COMPONENTS ${_py_components}) + endif () + # The version that was found may not be the default or user # defined one. set (PYTHON_VERSION_FOUND ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}) @@ -63,15 +91,44 @@ macro (find_python) set (PythonInterp3_FIND_VERSION PYTHON_VERSION_FOUND) set (PythonInterp3_FIND_VERSION_MAJOR ${Python3_VERSION_MAJOR}) + if (NOT DEFINED PYTHON_SITE_ROOT_DIR) + set (PYTHON_SITE_ROOT_DIR + "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages") + endif () if (NOT DEFINED PYTHON_SITE_DIR) - set (PYTHON_SITE_DIR "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages/OpenImageIO") + set (PYTHON_SITE_DIR "${PYTHON_SITE_ROOT_DIR}/OpenImageIO") endif () message (VERBOSE " Python site packages dir ${PYTHON_SITE_DIR}") + message (VERBOSE " Python site packages root ${PYTHON_SITE_ROOT_DIR}") message (VERBOSE " Python to include 'lib' prefix: ${PYLIB_LIB_PREFIX}") message (VERBOSE " Python to include SO version: ${PYLIB_INCLUDE_SONAME}") endmacro() +# Help CMake locate nanobind when it was installed as a Python package. +macro (discover_nanobind_cmake_dir) + if (nanobind_DIR OR nanobind_ROOT OR "$ENV{nanobind_DIR}" OR "$ENV{nanobind_ROOT}") + return() + endif () + + if (NOT Python3_Interpreter_FOUND) + return() + endif () + + execute_process ( + COMMAND ${Python3_EXECUTABLE} -m nanobind --cmake_dir + RESULT_VARIABLE _oiio_nanobind_result + OUTPUT_VARIABLE _oiio_nanobind_cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if (_oiio_nanobind_result EQUAL 0 + AND EXISTS "${_oiio_nanobind_cmake_dir}/nanobind-config.cmake") + set (nanobind_DIR "${_oiio_nanobind_cmake_dir}" CACHE PATH + "Path to the nanobind CMake package" FORCE) + endif () +endmacro() + + ########################################################################### # pybind11 @@ -163,3 +220,62 @@ macro (setup_python_module) endmacro () + +########################################################################### +# nanobind + +macro (setup_python_module_nanobind) + cmake_parse_arguments (lib "" "TARGET;MODULE" + "SOURCES;LIBS;INCLUDES;SYSTEM_INCLUDE_DIRS;PACKAGE_FILES" + ${ARGN}) + + set (target_name ${lib_TARGET}) + + if (NOT COMMAND nanobind_add_module) + discover_nanobind_cmake_dir() + find_package (nanobind CONFIG REQUIRED) + endif () + + nanobind_add_module(${target_name} ${lib_SOURCES}) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND TARGET nanobind-static) + target_compile_options (nanobind-static PRIVATE -Wno-error=format-nonliteral) + endif () + + target_include_directories (${target_name} + PRIVATE ${lib_INCLUDES}) + target_include_directories (${target_name} + SYSTEM PRIVATE ${lib_SYSTEM_INCLUDE_DIRS}) + target_link_libraries (${target_name} + PRIVATE ${lib_LIBS}) + + set (_module_LINK_FLAGS "${VISIBILITY_MAP_COMMAND} ${EXTRA_DSO_LINK_ARGS}") + if (UNIX AND NOT APPLE) + set (_module_LINK_FLAGS "${_module_LINK_FLAGS} -Wl,--exclude-libs,ALL") + endif () + set_target_properties (${target_name} PROPERTIES + LINK_FLAGS ${_module_LINK_FLAGS} + OUTPUT_NAME ${lib_MODULE} + DEBUG_POSTFIX "") + + if (SKBUILD) + set (_nanobind_install_dir .) + else () + set (_nanobind_install_dir ${PYTHON_SITE_DIR}) + endif () + + # Keep experimental modules isolated in the build tree so they don't alter + # how the existing top-level OpenImageIO module is imported during tests. + set_target_properties (${target_name} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental + ) + + install (TARGETS ${target_name} + RUNTIME DESTINATION ${_nanobind_install_dir} COMPONENT user + LIBRARY DESTINATION ${_nanobind_install_dir} COMPONENT user) + + if (lib_PACKAGE_FILES) + install (FILES ${lib_PACKAGE_FILES} + DESTINATION ${_nanobind_install_dir} COMPONENT user) + endif () +endmacro () diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index a954a83d21..6c2811361a 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -233,6 +233,9 @@ macro (oiio_add_all_tests) python-imageinput python-imagebufalgo IMAGEDIR oiio-images ) + if (OIIO_BUILD_PYTHON_NANOBIND) + oiio_add_tests (python-nanobind-experimental) + endif () endif () oiio_add_tests (oiiotool-color diff --git a/src/include/CMakeLists.txt b/src/include/CMakeLists.txt index 3ab45a5709..6a869bcdf6 100644 --- a/src/include/CMakeLists.txt +++ b/src/include/CMakeLists.txt @@ -50,6 +50,11 @@ install (FILES ${detail_headers} COMPONENT developer) if (OIIO_INTERNALIZE_FMT OR fmt_LOCAL_BUILD) + if (NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") + message (FATAL_ERROR + "OIIO_INTERNALIZE_FMT is enabled but FMT_INCLUDE_DIR could not " + "be determined from the imported fmt targets.") + endif () set (fmt_headers_base_names) foreach (header_name core.h format-inl.h format.h ostream.h printf.h std.h base.h chrono.h) @@ -57,6 +62,11 @@ if (OIIO_INTERNALIZE_FMT OR fmt_LOCAL_BUILD) list (APPEND fmt_headers_base_names ${header_name}) endif () endforeach () + if (NOT fmt_headers_base_names) + message (FATAL_ERROR + "OIIO_INTERNALIZE_FMT is enabled, but no fmt headers were found " + "under ${FMT_INCLUDE_DIR}/fmt") + endif () set (fmt_internal_directory ${CMAKE_BINARY_DIR}/include/OpenImageIO/detail/fmt) list (TRANSFORM fmt_headers_base_names PREPEND ${FMT_INCLUDE_DIR}/fmt/ diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt new file mode 100644 index 0000000000..122cabb9c7 --- /dev/null +++ b/src/python-nanobind/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +set (nanobind_experimental_srcs + nanobind_experimental.cpp) + +set (nanobind_package_files + nanobind_experimental.py) + +setup_python_module_nanobind ( + TARGET PyOpenImageIONanobindExperimental + MODULE _nanobind_experimental + SOURCES ${nanobind_experimental_srcs} + PACKAGE_FILES ${nanobind_package_files} +) + +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind") + install (FILES __init__.py + DESTINATION ${PYTHON_SITE_DIR} COMPONENT user) +endif () diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py new file mode 100644 index 0000000000..ebd77e164c --- /dev/null +++ b/src/python-nanobind/__init__.py @@ -0,0 +1,44 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +import os +import sys +import platform +import subprocess + +_here = os.path.abspath(os.path.dirname(__file__)) + +# Set $OpenImageIO_ROOT if not already set before importing helper modules. +if not os.getenv("OpenImageIO_ROOT"): + if all([os.path.exists(os.path.join(_here, i)) for i in ["share", "bin", "lib"]]): + os.environ["OpenImageIO_ROOT"] = _here + +if platform.system() == "Windows": + _bin_dir = os.path.join(_here, "bin") + if os.path.exists(_bin_dir): + os.add_dll_directory(_bin_dir) + elif sys.version_info >= (3, 8): + if os.getenv("OPENIMAGEIO_PYTHON_LOAD_DLLS_FROM_PATH", "0") == "1": + for path in os.getenv("PATH", "").split(os.pathsep): + if os.path.exists(path) and path != ".": + os.add_dll_directory(path) + +from . import nanobind_experimental # noqa: E402, F401 + +__doc__ = """ +OpenImageIO experimental Python package exposing nanobind migration shims. +The production bindings are not installed in this configuration. +"""[1:-1] + +__version__ = getattr(nanobind_experimental, "__version__", "") + + +def _call_program(name, args): + bin_dir = os.path.join(os.path.dirname(__file__), "bin") + return subprocess.call([os.path.join(bin_dir, name)] + args) + + +def _command_line(): + name = os.path.basename(sys.argv[0]) + raise SystemExit(_call_program(name, sys.argv[1:])) diff --git a/src/python-nanobind/nanobind_experimental.cpp b/src/python-nanobind/nanobind_experimental.cpp new file mode 100644 index 0000000000..aa00dad01e --- /dev/null +++ b/src/python-nanobind/nanobind_experimental.cpp @@ -0,0 +1,74 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include +#include +#include + +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; +OIIO_NAMESPACE_USING + +namespace { + +bool +roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) +{ + return roi.contains(x, y, z, ch); +} + + +bool +roi_contains_roi(const ROI& roi, const ROI& other) +{ + return roi.contains(other); +} + +} // namespace + + +NB_MODULE(_nanobind_experimental, m) +{ + m.doc() = "Experimental OpenImageIO nanobind bindings."; + + nb::class_ roi(m, "ROI"); + roi.def_rw("xbegin", &ROI::xbegin) + .def_rw("xend", &ROI::xend) + .def_rw("ybegin", &ROI::ybegin) + .def_rw("yend", &ROI::yend) + .def_rw("zbegin", &ROI::zbegin) + .def_rw("zend", &ROI::zend) + .def_rw("chbegin", &ROI::chbegin) + .def_rw("chend", &ROI::chend) + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def_prop_ro("defined", &ROI::defined) + .def_prop_ro("width", &ROI::width) + .def_prop_ro("height", &ROI::height) + .def_prop_ro("depth", &ROI::depth) + .def_prop_ro("nchannels", &ROI::nchannels) + .def_prop_ro("npixels", &ROI::npixels) + .def("contains", &roi_contains_coord, "x"_a, "y"_a, "z"_a = 0, + "ch"_a = 0) + .def("contains", &roi_contains_roi, "other"_a) + .def_prop_ro_static("All", [](nb::handle) { return ROI::All(); }) + .def("__str__", + [](const ROI& roi_) { + return Strutil::fmt::format("{}", roi_); + }) + .def("copy", [](const ROI& self) { return self; }) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + + m.def("union", &roi_union); + m.def("intersection", &roi_intersection); + m.attr("__version__") = OIIO_VERSION_STRING; +} diff --git a/src/python-nanobind/nanobind_experimental.py b/src/python-nanobind/nanobind_experimental.py new file mode 100644 index 0000000000..f24c4df92f --- /dev/null +++ b/src/python-nanobind/nanobind_experimental.py @@ -0,0 +1,5 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from ._nanobind_experimental import * # type: ignore # noqa: F401, F403 diff --git a/testsuite/python-nanobind-experimental/ref/out.txt b/testsuite/python-nanobind-experimental/ref/out.txt new file mode 100644 index 0000000000..54c26e0dd2 --- /dev/null +++ b/testsuite/python-nanobind-experimental/ref/out.txt @@ -0,0 +1,10 @@ +module: _nanobind_experimental +version: 3.2.0.1dev +roi: 1 4 2 6 0 1 0 10000 +contains (1,2): True +contains (5,2): False +contains other: False +union: 0 4 1 6 0 1 0 10000 +intersection: 1 2 2 3 0 1 0 10000 +ROI.All defined: False +copy equals: True diff --git a/testsuite/python-nanobind-experimental/run.py b/testsuite/python-nanobind-experimental/run.py new file mode 100644 index 0000000000..dc7e42ee98 --- /dev/null +++ b/testsuite/python-nanobind-experimental/run.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +command += pythonbin + " src/test_nanobind_experimental.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py new file mode 100644 index 0000000000..6ec1d24fca --- /dev/null +++ b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from __future__ import annotations + +import importlib.util +import pathlib +import sys + + +def load_module(build_root: pathlib.Path): + search_dirs = [ + build_root / "lib/python/nanobind-experimental", + build_root, + ] + matches = [] + for module_dir in search_dirs: + matches = sorted(module_dir.glob("_nanobind_experimental*")) + if matches: + break + if not matches: + raise RuntimeError(f"Could not find nanobind module in {search_dirs}") + + module_path = matches[0] + spec = importlib.util.spec_from_file_location("_nanobind_experimental", + module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load spec for {module_path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main() -> int: + build_root = pathlib.Path(sys.argv[1]).resolve() + nbexp = load_module(build_root) + + print("module:", nbexp.__name__) + print("version:", nbexp.__version__) + + roi = nbexp.ROI(1, 4, 2, 6) + other = nbexp.ROI(0, 2, 1, 3) + + print("roi:", roi) + print("contains (1,2):", roi.contains(1, 2)) + print("contains (5,2):", roi.contains(5, 2)) + print("contains other:", roi.contains(other)) + print("union:", nbexp.union(roi, other)) + print("intersection:", nbexp.intersection(roi, other)) + print("ROI.All defined:", nbexp.ROI.All.defined) + print("copy equals:", roi.copy() == roi) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From d7a7830603ffe3a11e62348324476eb003739d6d Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 10:01:56 +1100 Subject: [PATCH 02/26] A bit of flavour Signed-off-by: Aleksandr Motsjonov --- src/cmake/pythonutils.cmake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cmake/pythonutils.cmake b/src/cmake/pythonutils.cmake index ae63bc9385..6724ab6213 100644 --- a/src/cmake/pythonutils.cmake +++ b/src/cmake/pythonutils.cmake @@ -12,11 +12,17 @@ set (OIIO_PYTHON_BINDINGS_BACKEND "pybind11" CACHE STRING "Which Python binding backend(s) to build: pybind11, nanobind, or both") set_property (CACHE OIIO_PYTHON_BINDINGS_BACKEND PROPERTY STRINGS pybind11 nanobind both) + +# Normalize and validate the user-facing backend selector early so the rest +# of the file can make simple boolean decisions. string (TOLOWER "${OIIO_PYTHON_BINDINGS_BACKEND}" OIIO_PYTHON_BINDINGS_BACKEND) if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind|both)$") message (FATAL_ERROR "OIIO_PYTHON_BINDINGS_BACKEND must be one of: pybind11, nanobind, both") endif () + +# Derive internal switches used by the top-level CMakeLists and the Python +# helper macros below. set (OIIO_BUILD_PYTHON_PYBIND11 OFF) set (OIIO_BUILD_PYTHON_NANOBIND OFF) if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11" From 210cde57fae86c9fc7ac6d0d86a3ba0283b376ba Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 10:05:13 +1100 Subject: [PATCH 03/26] Revert FMT changes Signed-off-by: Aleksandr Motsjonov --- src/cmake/externalpackages.cmake | 35 +------------------------------- src/include/CMakeLists.txt | 10 --------- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index dfa7cfd85f..c5eabd749b 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -242,40 +242,7 @@ checked_find_package (fmt REQUIRED VERSION_MIN 9.0 BUILD_LOCAL missing ) -unset (FMT_INCLUDE_DIR) -if (TARGET fmt::fmt-header-only) - get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only - INTERFACE_INCLUDE_DIRECTORIES) -endif () -if ((NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") - AND TARGET fmt::fmt) - get_target_property(FMT_INCLUDE_DIR fmt::fmt - INTERFACE_INCLUDE_DIRECTORIES) -endif () -if (FMT_INCLUDE_DIR AND ";" IN_LIST FMT_INCLUDE_DIR) - list (GET FMT_INCLUDE_DIR 0 FMT_INCLUDE_DIR) -endif () -foreach (_fmt_include_var fmt_INCLUDE_DIRS fmt_INCLUDE_DIR FMT_INCLUDE_DIRS FMT_INCLUDE_DIR) - if (NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") - if (DEFINED ${_fmt_include_var} AND NOT "${${_fmt_include_var}}" STREQUAL "") - set (FMT_INCLUDE_DIR "${${_fmt_include_var}}") - endif () - endif () -endforeach () -if (FMT_INCLUDE_DIR AND ";" IN_LIST FMT_INCLUDE_DIR) - list (GET FMT_INCLUDE_DIR 0 FMT_INCLUDE_DIR) -endif () -if ((NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") - AND fmt_DIR) - get_filename_component (_fmt_prefix "${fmt_DIR}" DIRECTORY) - get_filename_component (_fmt_prefix "${_fmt_prefix}" DIRECTORY) - get_filename_component (_fmt_prefix "${_fmt_prefix}" DIRECTORY) - if (EXISTS "${_fmt_prefix}/include/fmt") - set (FMT_INCLUDE_DIR "${_fmt_prefix}/include") - endif () -endif () -unset (_fmt_include_var) -unset (_fmt_prefix) +get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only INTERFACE_INCLUDE_DIRECTORIES) ########################################################################### diff --git a/src/include/CMakeLists.txt b/src/include/CMakeLists.txt index 6a869bcdf6..3ab45a5709 100644 --- a/src/include/CMakeLists.txt +++ b/src/include/CMakeLists.txt @@ -50,11 +50,6 @@ install (FILES ${detail_headers} COMPONENT developer) if (OIIO_INTERNALIZE_FMT OR fmt_LOCAL_BUILD) - if (NOT FMT_INCLUDE_DIR OR FMT_INCLUDE_DIR STREQUAL "FMT_INCLUDE_DIR-NOTFOUND") - message (FATAL_ERROR - "OIIO_INTERNALIZE_FMT is enabled but FMT_INCLUDE_DIR could not " - "be determined from the imported fmt targets.") - endif () set (fmt_headers_base_names) foreach (header_name core.h format-inl.h format.h ostream.h printf.h std.h base.h chrono.h) @@ -62,11 +57,6 @@ if (OIIO_INTERNALIZE_FMT OR fmt_LOCAL_BUILD) list (APPEND fmt_headers_base_names ${header_name}) endif () endforeach () - if (NOT fmt_headers_base_names) - message (FATAL_ERROR - "OIIO_INTERNALIZE_FMT is enabled, but no fmt headers were found " - "under ${FMT_INCLUDE_DIR}/fmt") - endif () set (fmt_internal_directory ${CMAKE_BINARY_DIR}/include/OpenImageIO/detail/fmt) list (TRANSFORM fmt_headers_base_names PREPEND ${FMT_INCLUDE_DIR}/fmt/ From 7a703ae5759fcce2e6e4361c03dd6f53c29b4879 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 10:12:14 +1100 Subject: [PATCH 04/26] formatter Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/nanobind_experimental.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/python-nanobind/nanobind_experimental.cpp b/src/python-nanobind/nanobind_experimental.cpp index aa00dad01e..0940d23811 100644 --- a/src/python-nanobind/nanobind_experimental.cpp +++ b/src/python-nanobind/nanobind_experimental.cpp @@ -61,9 +61,7 @@ NB_MODULE(_nanobind_experimental, m) .def("contains", &roi_contains_roi, "other"_a) .def_prop_ro_static("All", [](nb::handle) { return ROI::All(); }) .def("__str__", - [](const ROI& roi_) { - return Strutil::fmt::format("{}", roi_); - }) + [](const ROI& roi_) { return Strutil::fmt::format("{}", roi_); }) .def("copy", [](const ROI& self) { return self; }) .def(nb::self == nb::self) .def(nb::self != nb::self); From c9f7a85520075736bd2c05e2fbc82ded5032ba14 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 10:45:44 +1100 Subject: [PATCH 05/26] Remove python shim Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/CMakeLists.txt | 4 ---- src/python-nanobind/__init__.py | 7 ++++--- src/python-nanobind/nanobind_experimental.py | 5 ----- 3 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 src/python-nanobind/nanobind_experimental.py diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 122cabb9c7..739a5e8712 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -5,14 +5,10 @@ set (nanobind_experimental_srcs nanobind_experimental.cpp) -set (nanobind_package_files - nanobind_experimental.py) - setup_python_module_nanobind ( TARGET PyOpenImageIONanobindExperimental MODULE _nanobind_experimental SOURCES ${nanobind_experimental_srcs} - PACKAGE_FILES ${nanobind_package_files} ) if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind") diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py index ebd77e164c..ffa83439fc 100644 --- a/src/python-nanobind/__init__.py +++ b/src/python-nanobind/__init__.py @@ -24,14 +24,15 @@ if os.path.exists(path) and path != ".": os.add_dll_directory(path) -from . import nanobind_experimental # noqa: E402, F401 +from . import _nanobind_experimental as _ext # noqa: E402 +from ._nanobind_experimental import * # type: ignore # noqa: E402, F401, F403 __doc__ = """ -OpenImageIO experimental Python package exposing nanobind migration shims. +OpenImageIO experimental Python package exposing nanobind migration bindings. The production bindings are not installed in this configuration. """[1:-1] -__version__ = getattr(nanobind_experimental, "__version__", "") +__version__ = getattr(_ext, "__version__", "") def _call_program(name, args): diff --git a/src/python-nanobind/nanobind_experimental.py b/src/python-nanobind/nanobind_experimental.py deleted file mode 100644 index f24c4df92f..0000000000 --- a/src/python-nanobind/nanobind_experimental.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from ._nanobind_experimental import * # type: ignore # noqa: F401, F403 From 86a6a8e3817245f500300f918c65bdb34530809f Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 10:48:45 +1100 Subject: [PATCH 06/26] changing from extension testing to package Signed-off-by: Aleksandr Motsjonov --- src/cmake/pythonutils.cmake | 4 +-- src/python-nanobind/CMakeLists.txt | 7 +++++ .../python-nanobind-experimental/ref/out.txt | 2 +- .../src/test_nanobind_experimental.py | 31 +++++-------------- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/cmake/pythonutils.cmake b/src/cmake/pythonutils.cmake index 6724ab6213..d85931e427 100644 --- a/src/cmake/pythonutils.cmake +++ b/src/cmake/pythonutils.cmake @@ -272,8 +272,8 @@ macro (setup_python_module_nanobind) # Keep experimental modules isolated in the build tree so they don't alter # how the existing top-level OpenImageIO module is imported during tests. set_target_properties (${target_name} PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental - ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO ) install (TARGETS ${target_name} diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 739a5e8712..28627bcc99 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -5,6 +5,13 @@ set (nanobind_experimental_srcs nanobind_experimental.cpp) +set (nanobind_build_package_dir + ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO) +file (MAKE_DIRECTORY ${nanobind_build_package_dir}) +configure_file (__init__.py + ${nanobind_build_package_dir}/__init__.py + COPYONLY) + setup_python_module_nanobind ( TARGET PyOpenImageIONanobindExperimental MODULE _nanobind_experimental diff --git a/testsuite/python-nanobind-experimental/ref/out.txt b/testsuite/python-nanobind-experimental/ref/out.txt index 54c26e0dd2..3d2fe75b2b 100644 --- a/testsuite/python-nanobind-experimental/ref/out.txt +++ b/testsuite/python-nanobind-experimental/ref/out.txt @@ -1,4 +1,4 @@ -module: _nanobind_experimental +module: OpenImageIO version: 3.2.0.1dev roi: 1 4 2 6 0 1 0 10000 contains (1,2): True diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py index 6ec1d24fca..0cc4ec9ba4 100644 --- a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py +++ b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py @@ -6,38 +6,23 @@ from __future__ import annotations -import importlib.util +import importlib import pathlib import sys -def load_module(build_root: pathlib.Path): - search_dirs = [ - build_root / "lib/python/nanobind-experimental", - build_root, - ] - matches = [] - for module_dir in search_dirs: - matches = sorted(module_dir.glob("_nanobind_experimental*")) - if matches: - break - if not matches: - raise RuntimeError(f"Could not find nanobind module in {search_dirs}") +def load_package(build_root: pathlib.Path): + package_root = build_root / "lib/python/nanobind-experimental" + if not (package_root / "OpenImageIO" / "__init__.py").exists(): + raise RuntimeError(f"Could not find OpenImageIO package in {package_root}") - module_path = matches[0] - spec = importlib.util.spec_from_file_location("_nanobind_experimental", - module_path) - if spec is None or spec.loader is None: - raise RuntimeError(f"Could not load spec for {module_path}") - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module + sys.path.insert(0, str(package_root)) + return importlib.import_module("OpenImageIO") def main() -> int: build_root = pathlib.Path(sys.argv[1]).resolve() - nbexp = load_module(build_root) + nbexp = load_package(build_root) print("module:", nbexp.__name__) print("version:", nbexp.__version__) From 0bc9aff5f36f78d166401d1803ce3e12e50d312f Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 17:59:30 +1100 Subject: [PATCH 07/26] add more of ROI support Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/CMakeLists.txt | 1 + src/python-nanobind/nanobind_experimental.cpp | 51 +++++++++++++++++++ .../python-nanobind-experimental/ref/out.txt | 4 ++ .../src/test_nanobind_experimental.py | 22 ++++++++ 4 files changed, 78 insertions(+) diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 28627bcc99..9d3dfb7925 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -16,6 +16,7 @@ setup_python_module_nanobind ( TARGET PyOpenImageIONanobindExperimental MODULE _nanobind_experimental SOURCES ${nanobind_experimental_srcs} + LIBS OpenImageIO ) if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind") diff --git a/src/python-nanobind/nanobind_experimental.cpp b/src/python-nanobind/nanobind_experimental.cpp index 0940d23811..81d063a819 100644 --- a/src/python-nanobind/nanobind_experimental.cpp +++ b/src/python-nanobind/nanobind_experimental.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/AcademySoftwareFoundation/OpenImageIO +#include #include #include #include @@ -29,6 +30,34 @@ roi_contains_roi(const ROI& roi, const ROI& other) return roi.contains(other); } + +ROI +imagespec_get_roi(const ImageSpec& spec) +{ + return get_roi(spec); +} + + +ROI +imagespec_get_roi_full(const ImageSpec& spec) +{ + return get_roi_full(spec); +} + + +void +imagespec_set_roi(ImageSpec& spec, const ROI& roi) +{ + set_roi(spec, roi); +} + + +void +imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) +{ + set_roi_full(spec, roi); +} + } // namespace @@ -66,7 +95,29 @@ NB_MODULE(_nanobind_experimental, m) .def(nb::self == nb::self) .def(nb::self != nb::self); + nb::class_(m, "ImageSpec") + .def(nb::init<>()) + .def_rw("x", &ImageSpec::x) + .def_rw("y", &ImageSpec::y) + .def_rw("z", &ImageSpec::z) + .def_rw("width", &ImageSpec::width) + .def_rw("height", &ImageSpec::height) + .def_rw("depth", &ImageSpec::depth) + .def_rw("full_x", &ImageSpec::full_x) + .def_rw("full_y", &ImageSpec::full_y) + .def_rw("full_z", &ImageSpec::full_z) + .def_rw("full_width", &ImageSpec::full_width) + .def_rw("full_height", &ImageSpec::full_height) + .def_rw("full_depth", &ImageSpec::full_depth) + .def_rw("nchannels", &ImageSpec::nchannels) + .def_prop_ro("roi", &imagespec_get_roi) + .def_prop_ro("roi_full", &imagespec_get_roi_full); + m.def("union", &roi_union); m.def("intersection", &roi_intersection); + m.def("get_roi", &get_roi); + m.def("get_roi_full", &get_roi_full); + m.def("set_roi", &imagespec_set_roi); + m.def("set_roi_full", &imagespec_set_roi_full); m.attr("__version__") = OIIO_VERSION_STRING; } diff --git a/testsuite/python-nanobind-experimental/ref/out.txt b/testsuite/python-nanobind-experimental/ref/out.txt index 3d2fe75b2b..dcb501c6f0 100644 --- a/testsuite/python-nanobind-experimental/ref/out.txt +++ b/testsuite/python-nanobind-experimental/ref/out.txt @@ -8,3 +8,7 @@ union: 0 4 1 6 0 1 0 10000 intersection: 1 2 2 3 0 1 0 10000 ROI.All defined: False copy equals: True +spec roi: 10 40 20 60 0 1 0 3 +spec roi_full: 8 42 18 62 0 1 0 3 +updated roi: 12 16 22 26 0 1 0 3 +updated roi_full: 7 19 17 29 0 1 0 3 diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py index 0cc4ec9ba4..61905534ea 100644 --- a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py +++ b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py @@ -38,6 +38,28 @@ def main() -> int: print("intersection:", nbexp.intersection(roi, other)) print("ROI.All defined:", nbexp.ROI.All.defined) print("copy equals:", roi.copy() == roi) + + spec = nbexp.ImageSpec() + spec.x = 10 + spec.y = 20 + spec.z = 0 + spec.width = 30 + spec.height = 40 + spec.depth = 1 + spec.nchannels = 3 + spec.full_x = 8 + spec.full_y = 18 + spec.full_z = 0 + spec.full_width = 34 + spec.full_height = 44 + spec.full_depth = 1 + + print("spec roi:", nbexp.get_roi(spec)) + print("spec roi_full:", nbexp.get_roi_full(spec)) + nbexp.set_roi(spec, nbexp.ROI(12, 16, 22, 26, 0, 1, 0, 2)) + nbexp.set_roi_full(spec, nbexp.ROI(7, 19, 17, 29, 0, 1, 0, 4)) + print("updated roi:", nbexp.get_roi(spec)) + print("updated roi_full:", nbexp.get_roi_full(spec)) return 0 From 1d0f97f263c585d59ecd0f146d82cc802acaf90b Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 18:11:57 +1100 Subject: [PATCH 08/26] one python test file to rule them all Signed-off-by: Aleksandr Motsjonov --- testsuite/common/roi_shared.py | 89 +++++++++++++++++++ .../python-nanobind-experimental/ref/out.txt | 56 +++++++++--- .../src/test_nanobind_experimental.py | 45 ++-------- testsuite/python-roi/src/test_roi.py | 79 ++-------------- 4 files changed, 148 insertions(+), 121 deletions(-) create mode 100644 testsuite/common/roi_shared.py mode change 100755 => 100644 testsuite/python-roi/src/test_roi.py diff --git a/testsuite/common/roi_shared.py b/testsuite/common/roi_shared.py new file mode 100644 index 0000000000..56ca8b759e --- /dev/null +++ b/testsuite/common/roi_shared.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from __future__ import annotations + + +def run(oiio): + r = oiio.ROI() + print("undefined ROI() =", r) + print("r.defined =", r.defined) + print("r.nchannels =", r.nchannels) + print("") + + r = oiio.ROI(0, 640, 100, 200) + print("ROI(0, 640, 100, 200) =", r) + r = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + print("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) + print("r.xbegin =", r.xbegin) + print("r.xend =", r.xend) + print("r.ybegin =", r.ybegin) + print("r.yend =", r.yend) + print("r.zbegin =", r.zbegin) + print("r.zend =", r.zend) + print("r.chbegin =", r.chbegin) + print("r.chend =", r.chend) + print("r.defined = ", r.defined) + print("r.width = ", r.width) + print("r.height = ", r.height) + print("r.depth = ", r.depth) + print("r.nchannels = ", r.nchannels) + print("r.npixels = ", r.npixels) + print("") + print("ROI.All =", oiio.ROI.All) + print("") + + r2 = oiio.ROI(r) + r3 = oiio.ROI(r) + r3.xend = 320 + print("r == r2 (expect yes): ", (r == r2)) + print("r != r2 (expect no): ", (r != r2)) + print("r == r3 (expect no): ", (r == r3)) + print("r != r3 (expect yes): ", (r != r3)) + print("") + + print("r contains (10,10) (expect yes): ", r.contains(10, 10)) + print("r contains (1000,10) (expect no): ", r.contains(1000, 10)) + print("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", + r.contains(oiio.ROI(10, 20, 10, 20, 0, 1, 0, 1))) + print("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", + r.contains(oiio.ROI(1010, 1020, 10, 20, 0, 1, 0, 1))) + + a_roi = oiio.ROI(0, 10, 0, 8, 0, 1, 0, 4) + b_roi = oiio.ROI(5, 15, -1, 10, 0, 1, 0, 4) + print("A =", a_roi) + print("B =", b_roi) + print("ROI.union(A,B) =", oiio.union(a_roi, b_roi)) + print("ROI.intersection(A,B) =", oiio.intersection(a_roi, b_roi)) + print("") + + spec = oiio.ImageSpec() + spec.x = 0 + spec.y = 0 + spec.z = 0 + spec.width = 640 + spec.height = 480 + spec.depth = 1 + spec.full_x = 0 + spec.full_y = 0 + spec.full_z = 0 + spec.full_width = 640 + spec.full_height = 480 + spec.full_depth = 1 + spec.nchannels = 3 + print("Spec's roi is", oiio.get_roi(spec)) + oiio.set_roi(spec, oiio.ROI(3, 5, 7, 9)) + oiio.set_roi_full(spec, oiio.ROI(13, 15, 17, 19)) + print("After set, roi is", oiio.get_roi(spec)) + print("After set, roi_full is", oiio.get_roi_full(spec)) + + r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + r2 = r1.copy() + r2.xbegin = 42 + print("r1 =", r1) + print("r2 =", r2) + print("") + print("Done.") diff --git a/testsuite/python-nanobind-experimental/ref/out.txt b/testsuite/python-nanobind-experimental/ref/out.txt index dcb501c6f0..469009902b 100644 --- a/testsuite/python-nanobind-experimental/ref/out.txt +++ b/testsuite/python-nanobind-experimental/ref/out.txt @@ -1,14 +1,46 @@ module: OpenImageIO version: 3.2.0.1dev -roi: 1 4 2 6 0 1 0 10000 -contains (1,2): True -contains (5,2): False -contains other: False -union: 0 4 1 6 0 1 0 10000 -intersection: 1 2 2 3 0 1 0 10000 -ROI.All defined: False -copy equals: True -spec roi: 10 40 20 60 0 1 0 3 -spec roi_full: 8 42 18 62 0 1 0 3 -updated roi: 12 16 22 26 0 1 0 3 -updated roi_full: 7 19 17 29 0 1 0 3 +undefined ROI() = -2147483648 0 0 0 0 0 0 0 +r.defined = False +r.nchannels = 0 + +ROI(0, 640, 100, 200) = 0 640 100 200 0 1 0 10000 +ROI(0, 640, 100, 480, 0, 1, 0, 4) = 0 640 0 480 0 1 0 4 +r.xbegin = 0 +r.xend = 640 +r.ybegin = 0 +r.yend = 480 +r.zbegin = 0 +r.zend = 1 +r.chbegin = 0 +r.chend = 4 +r.defined = True +r.width = 640 +r.height = 480 +r.depth = 1 +r.nchannels = 4 +r.npixels = 307200 + +ROI.All = -2147483648 0 0 0 0 0 0 0 + +r == r2 (expect yes): True +r != r2 (expect no): False +r == r3 (expect no): False +r != r3 (expect yes): True + +r contains (10,10) (expect yes): True +r contains (1000,10) (expect no): False +r contains roi(10,20,10,20,0,1,0,1) (expect yes): True +r contains roi(1010,1020,10,20,0,1,0,1) (expect no): False +A = 0 10 0 8 0 1 0 4 +B = 5 15 -1 10 0 1 0 4 +ROI.union(A,B) = 0 15 -1 10 0 1 0 4 +ROI.intersection(A,B) = 5 10 0 8 0 1 0 4 + +Spec's roi is 0 640 0 480 0 1 0 3 +After set, roi is 3 5 7 9 0 1 0 3 +After set, roi_full is 13 15 17 19 0 1 0 3 +r1 = 0 640 0 480 0 1 0 4 +r2 = 42 640 0 480 0 1 0 4 + +Done. diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py index 61905534ea..0bf83d0952 100644 --- a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py +++ b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py @@ -10,6 +10,10 @@ import pathlib import sys +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) + +from roi_shared import run # noqa: E402 + def load_package(build_root: pathlib.Path): package_root = build_root / "lib/python/nanobind-experimental" @@ -22,44 +26,11 @@ def load_package(build_root: pathlib.Path): def main() -> int: build_root = pathlib.Path(sys.argv[1]).resolve() - nbexp = load_package(build_root) - - print("module:", nbexp.__name__) - print("version:", nbexp.__version__) - - roi = nbexp.ROI(1, 4, 2, 6) - other = nbexp.ROI(0, 2, 1, 3) - - print("roi:", roi) - print("contains (1,2):", roi.contains(1, 2)) - print("contains (5,2):", roi.contains(5, 2)) - print("contains other:", roi.contains(other)) - print("union:", nbexp.union(roi, other)) - print("intersection:", nbexp.intersection(roi, other)) - print("ROI.All defined:", nbexp.ROI.All.defined) - print("copy equals:", roi.copy() == roi) - - spec = nbexp.ImageSpec() - spec.x = 10 - spec.y = 20 - spec.z = 0 - spec.width = 30 - spec.height = 40 - spec.depth = 1 - spec.nchannels = 3 - spec.full_x = 8 - spec.full_y = 18 - spec.full_z = 0 - spec.full_width = 34 - spec.full_height = 44 - spec.full_depth = 1 + oiio = load_package(build_root) - print("spec roi:", nbexp.get_roi(spec)) - print("spec roi_full:", nbexp.get_roi_full(spec)) - nbexp.set_roi(spec, nbexp.ROI(12, 16, 22, 26, 0, 1, 0, 2)) - nbexp.set_roi_full(spec, nbexp.ROI(7, 19, 17, 29, 0, 1, 0, 4)) - print("updated roi:", nbexp.get_roi(spec)) - print("updated roi_full:", nbexp.get_roi_full(spec)) + print("module:", oiio.__name__) + print("version:", oiio.__version__) + run(oiio) return 0 diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py old mode 100755 new mode 100644 index 3534e11458..fd55db75cf --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -6,82 +6,17 @@ from __future__ import annotations -import OpenImageIO as oiio - +import pathlib +import sys +import OpenImageIO as oiio +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) +from roi_shared import run # noqa: E402 -###################################################################### -# main test starts here try: - r = oiio.ROI() - print ("undefined ROI() =", r) - print ("r.defined =", r.defined) - print ("r.nchannels =", r.nchannels) - print ("") - - r = oiio.ROI (0, 640, 100, 200) - print ("ROI(0, 640, 100, 200) =", r) - r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) - print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) - print ("r.xbegin =", r.xbegin) - print ("r.xend =", r.xend) - print ("r.ybegin =", r.ybegin) - print ("r.yend =", r.yend) - print ("r.zbegin =", r.zbegin) - print ("r.zend =", r.zend) - print ("r.chbegin =", r.chbegin) - print ("r.chend =", r.chend) - print ("r.defined = ", r.defined) - print ("r.width = ", r.width) - print ("r.height = ", r.height) - print ("r.depth = ", r.depth) - print ("r.nchannels = ", r.nchannels) - print ("r.npixels = ", r.npixels) - print ("") - print ("ROI.All =", oiio.ROI.All) - print ("") - - r2 = oiio.ROI(r) - r3 = oiio.ROI(r) - r3.xend = 320 - print ("r == r2 (expect yes): ", (r == r2)) - print ("r != r2 (expect no): ", (r != r2)) - print ("r == r3 (expect no): ", (r == r3)) - print ("r != r3 (expect yes): ", (r != r3)) - print ("") - - print ("r contains (10,10) (expect yes): ", r.contains(10,10)) - print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) - print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) - print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) - - A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) - B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) - print ("A =", A) - print ("B =", B) - print ("ROI.union(A,B) =", oiio.union(A,B)) - print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) - print ("") - - spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) - print ("Spec's roi is", oiio.get_roi(spec)) - oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) - oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) - print ("After set, roi is", oiio.get_roi(spec)) - print ("After set, roi_full is", oiio.get_roi_full(spec)) - - r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - r2 = r1.copy() - r2.xbegin = 42 - print ("r1 =", r1) - print ("r2 =", r2) - - print ("") - - print ("Done.") + run(oiio) except Exception as detail: - print ("Unknown exception:", detail) - + print("Unknown exception:", detail) From 2427ca1b9166f80b3a1c3f54a2282d5cf83baea6 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 18:15:58 +1100 Subject: [PATCH 09/26] push things around for less of a changeset Signed-off-by: Aleksandr Motsjonov --- testsuite/common/roi_shared.py | 89 ----------------- .../src/test_nanobind_experimental.py | 6 +- testsuite/python-roi/src/test_roi.py | 98 +++++++++++++++++-- 3 files changed, 93 insertions(+), 100 deletions(-) delete mode 100644 testsuite/common/roi_shared.py diff --git a/testsuite/common/roi_shared.py b/testsuite/common/roi_shared.py deleted file mode 100644 index 56ca8b759e..0000000000 --- a/testsuite/common/roi_shared.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from __future__ import annotations - - -def run(oiio): - r = oiio.ROI() - print("undefined ROI() =", r) - print("r.defined =", r.defined) - print("r.nchannels =", r.nchannels) - print("") - - r = oiio.ROI(0, 640, 100, 200) - print("ROI(0, 640, 100, 200) =", r) - r = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - print("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) - print("r.xbegin =", r.xbegin) - print("r.xend =", r.xend) - print("r.ybegin =", r.ybegin) - print("r.yend =", r.yend) - print("r.zbegin =", r.zbegin) - print("r.zend =", r.zend) - print("r.chbegin =", r.chbegin) - print("r.chend =", r.chend) - print("r.defined = ", r.defined) - print("r.width = ", r.width) - print("r.height = ", r.height) - print("r.depth = ", r.depth) - print("r.nchannels = ", r.nchannels) - print("r.npixels = ", r.npixels) - print("") - print("ROI.All =", oiio.ROI.All) - print("") - - r2 = oiio.ROI(r) - r3 = oiio.ROI(r) - r3.xend = 320 - print("r == r2 (expect yes): ", (r == r2)) - print("r != r2 (expect no): ", (r != r2)) - print("r == r3 (expect no): ", (r == r3)) - print("r != r3 (expect yes): ", (r != r3)) - print("") - - print("r contains (10,10) (expect yes): ", r.contains(10, 10)) - print("r contains (1000,10) (expect no): ", r.contains(1000, 10)) - print("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", - r.contains(oiio.ROI(10, 20, 10, 20, 0, 1, 0, 1))) - print("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", - r.contains(oiio.ROI(1010, 1020, 10, 20, 0, 1, 0, 1))) - - a_roi = oiio.ROI(0, 10, 0, 8, 0, 1, 0, 4) - b_roi = oiio.ROI(5, 15, -1, 10, 0, 1, 0, 4) - print("A =", a_roi) - print("B =", b_roi) - print("ROI.union(A,B) =", oiio.union(a_roi, b_roi)) - print("ROI.intersection(A,B) =", oiio.intersection(a_roi, b_roi)) - print("") - - spec = oiio.ImageSpec() - spec.x = 0 - spec.y = 0 - spec.z = 0 - spec.width = 640 - spec.height = 480 - spec.depth = 1 - spec.full_x = 0 - spec.full_y = 0 - spec.full_z = 0 - spec.full_width = 640 - spec.full_height = 480 - spec.full_depth = 1 - spec.nchannels = 3 - print("Spec's roi is", oiio.get_roi(spec)) - oiio.set_roi(spec, oiio.ROI(3, 5, 7, 9)) - oiio.set_roi_full(spec, oiio.ROI(13, 15, 17, 19)) - print("After set, roi is", oiio.get_roi(spec)) - print("After set, roi_full is", oiio.get_roi_full(spec)) - - r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - r2 = r1.copy() - r2.xbegin = 42 - print("r1 =", r1) - print("r2 =", r2) - print("") - print("Done.") diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py index 0bf83d0952..6cca49f510 100644 --- a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py +++ b/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py @@ -10,9 +10,11 @@ import pathlib import sys -sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) +sys.path.insert( + 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-roi" / "src") +) -from roi_shared import run # noqa: E402 +from test_roi import run # noqa: E402 def load_package(build_root: pathlib.Path): diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index fd55db75cf..59e2064a1f 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -6,17 +6,97 @@ from __future__ import annotations -import pathlib -import sys +def run(oiio): + r = oiio.ROI() + print("undefined ROI() =", r) + print("r.defined =", r.defined) + print("r.nchannels =", r.nchannels) + print("") -import OpenImageIO as oiio + r = oiio.ROI(0, 640, 100, 200) + print("ROI(0, 640, 100, 200) =", r) + r = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + print("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) + print("r.xbegin =", r.xbegin) + print("r.xend =", r.xend) + print("r.ybegin =", r.ybegin) + print("r.yend =", r.yend) + print("r.zbegin =", r.zbegin) + print("r.zend =", r.zend) + print("r.chbegin =", r.chbegin) + print("r.chend =", r.chend) + print("r.defined = ", r.defined) + print("r.width = ", r.width) + print("r.height = ", r.height) + print("r.depth = ", r.depth) + print("r.nchannels = ", r.nchannels) + print("r.npixels = ", r.npixels) + print("") + print("ROI.All =", oiio.ROI.All) + print("") -sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) + r2 = oiio.ROI(r) + r3 = oiio.ROI(r) + r3.xend = 320 + print("r == r2 (expect yes): ", (r == r2)) + print("r != r2 (expect no): ", (r != r2)) + print("r == r3 (expect no): ", (r == r3)) + print("r != r3 (expect yes): ", (r != r3)) + print("") -from roi_shared import run # noqa: E402 + print("r contains (10,10) (expect yes): ", r.contains(10, 10)) + print("r contains (1000,10) (expect no): ", r.contains(1000, 10)) + print("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", + r.contains(oiio.ROI(10, 20, 10, 20, 0, 1, 0, 1))) + print("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", + r.contains(oiio.ROI(1010, 1020, 10, 20, 0, 1, 0, 1))) + a_roi = oiio.ROI(0, 10, 0, 8, 0, 1, 0, 4) + b_roi = oiio.ROI(5, 15, -1, 10, 0, 1, 0, 4) + print("A =", a_roi) + print("B =", b_roi) + print("ROI.union(A,B) =", oiio.union(a_roi, b_roi)) + print("ROI.intersection(A,B) =", oiio.intersection(a_roi, b_roi)) + print("") -try: - run(oiio) -except Exception as detail: - print("Unknown exception:", detail) + spec = oiio.ImageSpec() + spec.x = 0 + spec.y = 0 + spec.z = 0 + spec.width = 640 + spec.height = 480 + spec.depth = 1 + spec.full_x = 0 + spec.full_y = 0 + spec.full_z = 0 + spec.full_width = 640 + spec.full_height = 480 + spec.full_depth = 1 + spec.nchannels = 3 + print("Spec's roi is", oiio.get_roi(spec)) + oiio.set_roi(spec, oiio.ROI(3, 5, 7, 9)) + oiio.set_roi_full(spec, oiio.ROI(13, 15, 17, 19)) + print("After set, roi is", oiio.get_roi(spec)) + print("After set, roi_full is", oiio.get_roi_full(spec)) + + r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + r2 = r1.copy() + r2.xbegin = 42 + print("r1 =", r1) + print("r2 =", r2) + print("") + print("Done.") + + +def main() -> int: + import OpenImageIO as oiio + + try: + run(oiio) + except Exception as detail: + print("Unknown exception:", detail) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From b9642ad78d95f51fb087ef3aa8a90172da5c8c83 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 18:34:52 +1100 Subject: [PATCH 10/26] No more experiments Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/CMakeLists.txt | 5 +- src/python-nanobind/__init__.py | 4 +- src/python-nanobind/py_oiio.cpp | 25 ++++++++ .../{nanobind_experimental.cpp => py_roi.cpp} | 64 ++++++++----------- 4 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 src/python-nanobind/py_oiio.cpp rename src/python-nanobind/{nanobind_experimental.cpp => py_roi.cpp} (78%) diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 9d3dfb7925..45728b37e6 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -3,7 +3,8 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO set (nanobind_experimental_srcs - nanobind_experimental.cpp) + py_oiio.cpp + py_roi.cpp) set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO) @@ -14,7 +15,7 @@ configure_file (__init__.py setup_python_module_nanobind ( TARGET PyOpenImageIONanobindExperimental - MODULE _nanobind_experimental + MODULE _OpenImageIO SOURCES ${nanobind_experimental_srcs} LIBS OpenImageIO ) diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py index ffa83439fc..c346e9a920 100644 --- a/src/python-nanobind/__init__.py +++ b/src/python-nanobind/__init__.py @@ -24,8 +24,8 @@ if os.path.exists(path) and path != ".": os.add_dll_directory(path) -from . import _nanobind_experimental as _ext # noqa: E402 -from ._nanobind_experimental import * # type: ignore # noqa: E402, F401, F403 +from . import _OpenImageIO as _ext # noqa: E402 +from ._OpenImageIO import * # type: ignore # noqa: E402, F401, F403 __doc__ = """ OpenImageIO experimental Python package exposing nanobind migration bindings. diff --git a/src/python-nanobind/py_oiio.cpp b/src/python-nanobind/py_oiio.cpp new file mode 100644 index 0000000000..9441879694 --- /dev/null +++ b/src/python-nanobind/py_oiio.cpp @@ -0,0 +1,25 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include + +#include + +namespace nb = nanobind; + +namespace PyOpenImageIO { + +void +declare_roi(nb::module_& m); + +} // namespace PyOpenImageIO + + +NB_MODULE(_OpenImageIO, m) +{ + m.doc() = "Experimental OpenImageIO nanobind bindings."; + + PyOpenImageIO::declare_roi(m); + m.attr("__version__") = OIIO_VERSION_STRING; +} diff --git a/src/python-nanobind/nanobind_experimental.cpp b/src/python-nanobind/py_roi.cpp similarity index 78% rename from src/python-nanobind/nanobind_experimental.cpp rename to src/python-nanobind/py_roi.cpp index 81d063a819..02b9a32093 100644 --- a/src/python-nanobind/nanobind_experimental.cpp +++ b/src/python-nanobind/py_roi.cpp @@ -3,8 +3,6 @@ // https://github.com/AcademySoftwareFoundation/OpenImageIO #include -#include -#include #include #include @@ -13,58 +11,51 @@ namespace nb = nanobind; using namespace nb::literals; + +namespace PyOpenImageIO { + OIIO_NAMESPACE_USING namespace { -bool -roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) -{ - return roi.contains(x, y, z, ch); -} + bool roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) + { + return roi.contains(x, y, z, ch); + } -bool -roi_contains_roi(const ROI& roi, const ROI& other) -{ - return roi.contains(other); -} + bool roi_contains_roi(const ROI& roi, const ROI& other) + { + return roi.contains(other); + } -ROI -imagespec_get_roi(const ImageSpec& spec) -{ - return get_roi(spec); -} + ROI imagespec_get_roi(const ImageSpec& spec) { return get_roi(spec); } -ROI -imagespec_get_roi_full(const ImageSpec& spec) -{ - return get_roi_full(spec); -} + ROI imagespec_get_roi_full(const ImageSpec& spec) + { + return get_roi_full(spec); + } -void -imagespec_set_roi(ImageSpec& spec, const ROI& roi) -{ - set_roi(spec, roi); -} + void imagespec_set_roi(ImageSpec& spec, const ROI& roi) + { + set_roi(spec, roi); + } -void -imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) -{ - set_roi_full(spec, roi); -} + void imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) + { + set_roi_full(spec, roi); + } } // namespace -NB_MODULE(_nanobind_experimental, m) +void +declare_roi(nb::module_& m) { - m.doc() = "Experimental OpenImageIO nanobind bindings."; - nb::class_ roi(m, "ROI"); roi.def_rw("xbegin", &ROI::xbegin) .def_rw("xend", &ROI::xend) @@ -119,5 +110,6 @@ NB_MODULE(_nanobind_experimental, m) m.def("get_roi_full", &get_roi_full); m.def("set_roi", &imagespec_set_roi); m.def("set_roi_full", &imagespec_set_roi_full); - m.attr("__version__") = OIIO_VERSION_STRING; } + +} // namespace PyOpenImageIO From 65c54f27844dd2cae15bc9b1c1e7499426bcdfe1 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 18:51:53 +1100 Subject: [PATCH 11/26] even less experiment Signed-off-by: Aleksandr Motsjonov --- INSTALL.md | 6 +- src/cmake/pythonutils.cmake | 6 +- src/cmake/testing.cmake | 2 +- src/python-nanobind/CMakeLists.txt | 12 +-- src/python-nanobind/__init__.py | 4 +- src/python-nanobind/py_imagespec.cpp | 73 ++++++++++++++++++ src/python-nanobind/py_oiio.cpp | 16 +--- src/python-nanobind/py_oiio.h | 27 +++++++ src/python-nanobind/py_roi.cpp | 77 ++++--------------- .../ref/out.txt | 0 .../run.py | 2 +- .../src/test_nanobind.py} | 2 +- 12 files changed, 134 insertions(+), 93 deletions(-) create mode 100644 src/python-nanobind/py_imagespec.cpp create mode 100644 src/python-nanobind/py_oiio.h rename testsuite/{python-nanobind-experimental => python-nanobind}/ref/out.txt (100%) rename testsuite/{python-nanobind-experimental => python-nanobind}/run.py (64%) rename testsuite/{python-nanobind-experimental/src/test_nanobind_experimental.py => python-nanobind/src/test_nanobind.py} (93%) diff --git a/INSTALL.md b/INSTALL.md index 4b8e5867be..19567db3f4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -42,7 +42,7 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**. * Python >= 3.9 (tested through 3.13). * pybind11 >= 2.7 (tested through 3.0) * NumPy (tested through 2.2.4) - * For the experimental nanobind migration backend: + * For the nanobind (WIP) migration backend: * nanobind discoverable by CMake, or installed in the active Python environment so `python -m nanobind --cmake_dir` works * If you want support for PNG files: @@ -162,7 +162,7 @@ Make wrapper (`make PkgName_ROOT=...`). `OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python binding backend(s) to configure. `both` keeps the existing pybind11 module and -also builds the experimental nanobind module. +also builds the nanobind (WIP) module. `OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them unless you are a developer of OIIO or want to verify that your build @@ -254,7 +254,7 @@ Additionally, a few helpful modifiers alter some build-time options: | make USE_QT=0 ... | Skip anything that needs Qt | | make MYCC=xx MYCXX=yy ... | Use custom compilers | | make USE_PYTHON=0 ... | Don't build the Python binding | -| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | Build the existing pybind11 bindings and the experimental nanobind module | +| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | Build the existing pybind11 bindings and the nanobind (WIP) module | | make BUILD_SHARED_LIBS=0 | Build static library instead of shared | | make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies | | make LINKSTATIC=1 ... | Link with static external libraries when possible | diff --git a/src/cmake/pythonutils.cmake b/src/cmake/pythonutils.cmake index d85931e427..02a945e592 100644 --- a/src/cmake/pythonutils.cmake +++ b/src/cmake/pythonutils.cmake @@ -269,11 +269,11 @@ macro (setup_python_module_nanobind) set (_nanobind_install_dir ${PYTHON_SITE_DIR}) endif () - # Keep experimental modules isolated in the build tree so they don't alter + # Keep nanobind modules isolated in the build tree so they don't alter # how the existing top-level OpenImageIO module is imported during tests. set_target_properties (${target_name} PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO - ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO ) install (TARGETS ${target_name} diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 6c2811361a..3deb4c779f 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -234,7 +234,7 @@ macro (oiio_add_all_tests) IMAGEDIR oiio-images ) if (OIIO_BUILD_PYTHON_NANOBIND) - oiio_add_tests (python-nanobind-experimental) + oiio_add_tests (python-nanobind) endif () endif () diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 45728b37e6..42a0e3c94b 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -2,21 +2,21 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO -set (nanobind_experimental_srcs +set (nanobind_srcs py_oiio.cpp - py_roi.cpp) + py_roi.cpp + py_imagespec.cpp) -set (nanobind_build_package_dir - ${CMAKE_BINARY_DIR}/lib/python/nanobind-experimental/OpenImageIO) +set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO) file (MAKE_DIRECTORY ${nanobind_build_package_dir}) configure_file (__init__.py ${nanobind_build_package_dir}/__init__.py COPYONLY) setup_python_module_nanobind ( - TARGET PyOpenImageIONanobindExperimental + TARGET PyOpenImageIONanobind MODULE _OpenImageIO - SOURCES ${nanobind_experimental_srcs} + SOURCES ${nanobind_srcs} LIBS OpenImageIO ) diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py index c346e9a920..e263815e18 100644 --- a/src/python-nanobind/__init__.py +++ b/src/python-nanobind/__init__.py @@ -28,8 +28,8 @@ from ._OpenImageIO import * # type: ignore # noqa: E402, F401, F403 __doc__ = """ -OpenImageIO experimental Python package exposing nanobind migration bindings. -The production bindings are not installed in this configuration. +OpenImageIO Python package exposing the nanobind migration bindings. +The production pybind11 bindings are not installed in this configuration. """[1:-1] __version__ = getattr(_ext, "__version__", "") diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp new file mode 100644 index 0000000000..220254cbd8 --- /dev/null +++ b/src/python-nanobind/py_imagespec.cpp @@ -0,0 +1,73 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +ROI +imagespec_get_roi(const OpenImageIO::v3_1::ImageSpec& spec) +{ + return OpenImageIO::v3_1::get_roi(spec); +} + + +ROI +imagespec_get_roi_full(const OpenImageIO::v3_1::ImageSpec& spec) +{ + return OpenImageIO::v3_1::get_roi_full(spec); +} + + +void +imagespec_set_roi(OpenImageIO::v3_1::ImageSpec& spec, const ROI& roi) +{ + OpenImageIO::v3_1::set_roi(spec, roi); +} + + +void +imagespec_set_roi_full(OpenImageIO::v3_1::ImageSpec& spec, const ROI& roi) +{ + OpenImageIO::v3_1::set_roi_full(spec, roi); +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_imagespec(nb::module_& m) +{ + // This is intentionally not a full ImageSpec port yet. It only exists + // to support ROI parity until py_imagespec.cpp grows into the real + // binding. + nb::class_(m, "ImageSpec") + .def(nb::init<>()) + .def_rw("x", &ImageSpec::x) + .def_rw("y", &ImageSpec::y) + .def_rw("z", &ImageSpec::z) + .def_rw("width", &ImageSpec::width) + .def_rw("height", &ImageSpec::height) + .def_rw("depth", &ImageSpec::depth) + .def_rw("full_x", &ImageSpec::full_x) + .def_rw("full_y", &ImageSpec::full_y) + .def_rw("full_z", &ImageSpec::full_z) + .def_rw("full_width", &ImageSpec::full_width) + .def_rw("full_height", &ImageSpec::full_height) + .def_rw("full_depth", &ImageSpec::full_depth) + .def_rw("nchannels", &ImageSpec::nchannels) + .def_prop_ro("roi", &imagespec_get_roi) + .def_prop_ro("roi_full", &imagespec_get_roi_full); + + m.def("get_roi", &imagespec_get_roi); + m.def("get_roi_full", &imagespec_get_roi_full); + m.def("set_roi", &imagespec_set_roi); + m.def("set_roi_full", &imagespec_set_roi_full); +} + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_oiio.cpp b/src/python-nanobind/py_oiio.cpp index 9441879694..65e4eed3c1 100644 --- a/src/python-nanobind/py_oiio.cpp +++ b/src/python-nanobind/py_oiio.cpp @@ -2,24 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/AcademySoftwareFoundation/OpenImageIO -#include - -#include - -namespace nb = nanobind; - -namespace PyOpenImageIO { - -void -declare_roi(nb::module_& m); - -} // namespace PyOpenImageIO +#include "py_oiio.h" NB_MODULE(_OpenImageIO, m) { - m.doc() = "Experimental OpenImageIO nanobind bindings."; + m.doc() = "OpenImageIO nanobind bindings."; PyOpenImageIO::declare_roi(m); + PyOpenImageIO::declare_imagespec(m); m.attr("__version__") = OIIO_VERSION_STRING; } diff --git a/src/python-nanobind/py_oiio.h b/src/python-nanobind/py_oiio.h new file mode 100644 index 0000000000..f4ec3cfe79 --- /dev/null +++ b/src/python-nanobind/py_oiio.h @@ -0,0 +1,27 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace PyOpenImageIO { + +OIIO_NAMESPACE_USING + +void +declare_roi(nb::module_& m); +void +declare_imagespec(nb::module_& m); + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_roi.cpp b/src/python-nanobind/py_roi.cpp index 02b9a32093..8d85933fb1 100644 --- a/src/python-nanobind/py_roi.cpp +++ b/src/python-nanobind/py_roi.cpp @@ -2,57 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/AcademySoftwareFoundation/OpenImageIO -#include -#include - -#include -#include -#include - -namespace nb = nanobind; -using namespace nb::literals; - -namespace PyOpenImageIO { - -OIIO_NAMESPACE_USING +#include "py_oiio.h" namespace { - bool roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) - { - return roi.contains(x, y, z, ch); - } - - - bool roi_contains_roi(const ROI& roi, const ROI& other) - { - return roi.contains(other); - } - - - ROI imagespec_get_roi(const ImageSpec& spec) { return get_roi(spec); } - - - ROI imagespec_get_roi_full(const ImageSpec& spec) - { - return get_roi_full(spec); - } - +OIIO_NAMESPACE_USING - void imagespec_set_roi(ImageSpec& spec, const ROI& roi) - { - set_roi(spec, roi); - } +bool +roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) +{ + return roi.contains(x, y, z, ch); +} - void imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) - { - set_roi_full(spec, roi); - } +bool +roi_contains_roi(const ROI& roi, const ROI& other) +{ + return roi.contains(other); +} } // namespace +namespace PyOpenImageIO { + void declare_roi(nb::module_& m) { @@ -86,30 +59,8 @@ declare_roi(nb::module_& m) .def(nb::self == nb::self) .def(nb::self != nb::self); - nb::class_(m, "ImageSpec") - .def(nb::init<>()) - .def_rw("x", &ImageSpec::x) - .def_rw("y", &ImageSpec::y) - .def_rw("z", &ImageSpec::z) - .def_rw("width", &ImageSpec::width) - .def_rw("height", &ImageSpec::height) - .def_rw("depth", &ImageSpec::depth) - .def_rw("full_x", &ImageSpec::full_x) - .def_rw("full_y", &ImageSpec::full_y) - .def_rw("full_z", &ImageSpec::full_z) - .def_rw("full_width", &ImageSpec::full_width) - .def_rw("full_height", &ImageSpec::full_height) - .def_rw("full_depth", &ImageSpec::full_depth) - .def_rw("nchannels", &ImageSpec::nchannels) - .def_prop_ro("roi", &imagespec_get_roi) - .def_prop_ro("roi_full", &imagespec_get_roi_full); - m.def("union", &roi_union); m.def("intersection", &roi_intersection); - m.def("get_roi", &get_roi); - m.def("get_roi_full", &get_roi_full); - m.def("set_roi", &imagespec_set_roi); - m.def("set_roi_full", &imagespec_set_roi_full); } } // namespace PyOpenImageIO diff --git a/testsuite/python-nanobind-experimental/ref/out.txt b/testsuite/python-nanobind/ref/out.txt similarity index 100% rename from testsuite/python-nanobind-experimental/ref/out.txt rename to testsuite/python-nanobind/ref/out.txt diff --git a/testsuite/python-nanobind-experimental/run.py b/testsuite/python-nanobind/run.py similarity index 64% rename from testsuite/python-nanobind-experimental/run.py rename to testsuite/python-nanobind/run.py index dc7e42ee98..11b7ca6209 100644 --- a/testsuite/python-nanobind-experimental/run.py +++ b/testsuite/python-nanobind/run.py @@ -4,4 +4,4 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO -command += pythonbin + " src/test_nanobind_experimental.py " + OIIO_BUILD_ROOT + " > out.txt" +command += pythonbin + " src/test_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py b/testsuite/python-nanobind/src/test_nanobind.py similarity index 93% rename from testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py rename to testsuite/python-nanobind/src/test_nanobind.py index 6cca49f510..86993ac914 100644 --- a/testsuite/python-nanobind-experimental/src/test_nanobind_experimental.py +++ b/testsuite/python-nanobind/src/test_nanobind.py @@ -18,7 +18,7 @@ def load_package(build_root: pathlib.Path): - package_root = build_root / "lib/python/nanobind-experimental" + package_root = build_root / "lib/python/nanobind" if not (package_root / "OpenImageIO" / "__init__.py").exists(): raise RuntimeError(f"Could not find OpenImageIO package in {package_root}") From c02ef8c14740b1f6df973589e462afc0e8042839 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Fri, 13 Mar 2026 19:10:33 +1100 Subject: [PATCH 12/26] comment out unused cli related binding Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py index e263815e18..c7f5ccbabf 100644 --- a/src/python-nanobind/__init__.py +++ b/src/python-nanobind/__init__.py @@ -5,7 +5,6 @@ import os import sys import platform -import subprocess _here = os.path.abspath(os.path.dirname(__file__)) @@ -34,12 +33,5 @@ __version__ = getattr(_ext, "__version__", "") - -def _call_program(name, args): - bin_dir = os.path.join(os.path.dirname(__file__), "bin") - return subprocess.call([os.path.join(bin_dir, name)] + args) - - -def _command_line(): - name = os.path.basename(sys.argv[0]) - raise SystemExit(_call_program(name, sys.argv[1:])) +# TODO: Restore the Python CLI entry-point trampolines when the nanobind +# package ships the full wheel-style bin/lib/share layout. From 834ab9a58697d2b640a21a5b204c7c4a92f3c53d Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 12:08:42 +1100 Subject: [PATCH 13/26] revert some white spacing Signed-off-by: Aleksandr Motsjonov --- testsuite/python-roi/src/test_roi.py | 106 +++++++++++++-------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 59e2064a1f..8630c9d092 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -8,56 +8,56 @@ def run(oiio): r = oiio.ROI() - print("undefined ROI() =", r) - print("r.defined =", r.defined) - print("r.nchannels =", r.nchannels) - print("") + print ("undefined ROI() =", r) + print ("r.defined =", r.defined) + print ("r.nchannels =", r.nchannels) + print ("") - r = oiio.ROI(0, 640, 100, 200) - print("ROI(0, 640, 100, 200) =", r) - r = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - print("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) - print("r.xbegin =", r.xbegin) - print("r.xend =", r.xend) - print("r.ybegin =", r.ybegin) - print("r.yend =", r.yend) - print("r.zbegin =", r.zbegin) - print("r.zend =", r.zend) - print("r.chbegin =", r.chbegin) - print("r.chend =", r.chend) - print("r.defined = ", r.defined) - print("r.width = ", r.width) - print("r.height = ", r.height) - print("r.depth = ", r.depth) - print("r.nchannels = ", r.nchannels) - print("r.npixels = ", r.npixels) - print("") - print("ROI.All =", oiio.ROI.All) - print("") + r = oiio.ROI (0, 640, 100, 200) + print ("ROI(0, 640, 100, 200) =", r) + r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) + print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) + print ("r.xbegin =", r.xbegin) + print ("r.xend =", r.xend) + print ("r.ybegin =", r.ybegin) + print ("r.yend =", r.yend) + print ("r.zbegin =", r.zbegin) + print ("r.zend =", r.zend) + print ("r.chbegin =", r.chbegin) + print ("r.chend =", r.chend) + print ("r.defined = ", r.defined) + print ("r.width = ", r.width) + print ("r.height = ", r.height) + print ("r.depth = ", r.depth) + print ("r.nchannels = ", r.nchannels) + print ("r.npixels = ", r.npixels) + print ("") + print ("ROI.All =", oiio.ROI.All) + print ("") r2 = oiio.ROI(r) r3 = oiio.ROI(r) r3.xend = 320 - print("r == r2 (expect yes): ", (r == r2)) - print("r != r2 (expect no): ", (r != r2)) - print("r == r3 (expect no): ", (r == r3)) - print("r != r3 (expect yes): ", (r != r3)) - print("") + print ("r == r2 (expect yes): ", (r == r2)) + print ("r != r2 (expect no): ", (r != r2)) + print ("r == r3 (expect no): ", (r == r3)) + print ("r != r3 (expect yes): ", (r != r3)) + print ("") - print("r contains (10,10) (expect yes): ", r.contains(10, 10)) - print("r contains (1000,10) (expect no): ", r.contains(1000, 10)) - print("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", - r.contains(oiio.ROI(10, 20, 10, 20, 0, 1, 0, 1))) - print("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", - r.contains(oiio.ROI(1010, 1020, 10, 20, 0, 1, 0, 1))) + print ("r contains (10,10) (expect yes): ", r.contains(10,10)) + print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) + print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", + r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) + print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", + r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) - a_roi = oiio.ROI(0, 10, 0, 8, 0, 1, 0, 4) - b_roi = oiio.ROI(5, 15, -1, 10, 0, 1, 0, 4) - print("A =", a_roi) - print("B =", b_roi) - print("ROI.union(A,B) =", oiio.union(a_roi, b_roi)) - print("ROI.intersection(A,B) =", oiio.intersection(a_roi, b_roi)) - print("") + A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) + B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) + print ("A =", A) + print ("B =", B) + print ("ROI.union(A,B) =", oiio.union(A,B)) + print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) + print ("") spec = oiio.ImageSpec() spec.x = 0 @@ -73,19 +73,19 @@ def run(oiio): spec.full_height = 480 spec.full_depth = 1 spec.nchannels = 3 - print("Spec's roi is", oiio.get_roi(spec)) - oiio.set_roi(spec, oiio.ROI(3, 5, 7, 9)) - oiio.set_roi_full(spec, oiio.ROI(13, 15, 17, 19)) - print("After set, roi is", oiio.get_roi(spec)) - print("After set, roi_full is", oiio.get_roi_full(spec)) + print ("Spec's roi is", oiio.get_roi(spec)) + oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) + oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) + print ("After set, roi is", oiio.get_roi(spec)) + print ("After set, roi_full is", oiio.get_roi_full(spec)) r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) r2 = r1.copy() r2.xbegin = 42 - print("r1 =", r1) - print("r2 =", r2) - print("") - print("Done.") + print ("r1 =", r1) + print ("r2 =", r2) + print ("") + print ("Done.") def main() -> int: @@ -94,7 +94,7 @@ def main() -> int: try: run(oiio) except Exception as detail: - print("Unknown exception:", detail) + print ("Unknown exception:", detail) return 0 From aba87297d34e62d05616f0d4760e1459d7995df5 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 12:17:18 +1100 Subject: [PATCH 14/26] Less changes in the test Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/py_imagespec.cpp | 8 ++++++++ testsuite/python-roi/src/test_roi.py | 15 +-------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp index 220254cbd8..0cab3ede3a 100644 --- a/src/python-nanobind/py_imagespec.cpp +++ b/src/python-nanobind/py_imagespec.cpp @@ -48,6 +48,13 @@ declare_imagespec(nb::module_& m) // binding. nb::class_(m, "ImageSpec") .def(nb::init<>()) + .def("__init__", + [](ImageSpec* self, int xres, int yres, int nchans, int format) { + new (self) + ImageSpec(xres, yres, nchans, + TypeDesc(static_cast( + format))); + }) .def_rw("x", &ImageSpec::x) .def_rw("y", &ImageSpec::y) .def_rw("z", &ImageSpec::z) @@ -68,6 +75,7 @@ declare_imagespec(nb::module_& m) m.def("get_roi_full", &imagespec_get_roi_full); m.def("set_roi", &imagespec_set_roi); m.def("set_roi_full", &imagespec_set_roi_full); + m.attr("UINT8") = static_cast(TypeDesc::UINT8); } } // namespace PyOpenImageIO diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 8630c9d092..201d378f79 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -59,20 +59,7 @@ def run(oiio): print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) print ("") - spec = oiio.ImageSpec() - spec.x = 0 - spec.y = 0 - spec.z = 0 - spec.width = 640 - spec.height = 480 - spec.depth = 1 - spec.full_x = 0 - spec.full_y = 0 - spec.full_z = 0 - spec.full_width = 640 - spec.full_height = 480 - spec.full_depth = 1 - spec.nchannels = 3 + spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) print ("Spec's roi is", oiio.get_roi(spec)) oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) From c3a0870b6bfed7c2bee58e6dc3e4484de74a41d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 12:21:58 +1100 Subject: [PATCH 15/26] Less diff Signed-off-by: Aleksandr Motsjonov --- testsuite/python-roi/src/test_roi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 201d378f79..fba87adff8 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -46,10 +46,8 @@ def run(oiio): print ("r contains (10,10) (expect yes): ", r.contains(10,10)) print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) - print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", - r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) - print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", - r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) + print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) + print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) @@ -71,7 +69,9 @@ def run(oiio): r2.xbegin = 42 print ("r1 =", r1) print ("r2 =", r2) + print ("") + print ("Done.") From adbf44524acfe6c1d166e89ebd25fdf1d43c0344 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 13:16:21 +1100 Subject: [PATCH 16/26] better naming and extracting common bit Signed-off-by: Aleksandr Motsjonov --- src/cmake/testing.cmake | 2 +- .../pythonbinding_loaders.py} | 22 +-------- testsuite/python-nanobind/ref/out.txt | 46 ------------------- .../run.py | 3 +- .../src/test_roi_nanobind.py | 30 ++++++++++++ testsuite/python-roi/src/test_roi.py | 2 +- 6 files changed, 35 insertions(+), 70 deletions(-) rename testsuite/{python-nanobind/src/test_nanobind.py => common/pythonbinding_loaders.py} (54%) delete mode 100644 testsuite/python-nanobind/ref/out.txt rename testsuite/{python-nanobind => python-roi-nanobind}/run.py (50%) create mode 100644 testsuite/python-roi-nanobind/src/test_roi_nanobind.py diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 3deb4c779f..6fb174a98d 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -234,7 +234,7 @@ macro (oiio_add_all_tests) IMAGEDIR oiio-images ) if (OIIO_BUILD_PYTHON_NANOBIND) - oiio_add_tests (python-nanobind) + oiio_add_tests (python-roi-nanobind) endif () endif () diff --git a/testsuite/python-nanobind/src/test_nanobind.py b/testsuite/common/pythonbinding_loaders.py similarity index 54% rename from testsuite/python-nanobind/src/test_nanobind.py rename to testsuite/common/pythonbinding_loaders.py index 86993ac914..986917fdb8 100644 --- a/testsuite/python-nanobind/src/test_nanobind.py +++ b/testsuite/common/pythonbinding_loaders.py @@ -10,31 +10,11 @@ import pathlib import sys -sys.path.insert( - 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-roi" / "src") -) -from test_roi import run # noqa: E402 - - -def load_package(build_root: pathlib.Path): +def load_nanobind_oiio_package(build_root: pathlib.Path): package_root = build_root / "lib/python/nanobind" if not (package_root / "OpenImageIO" / "__init__.py").exists(): raise RuntimeError(f"Could not find OpenImageIO package in {package_root}") sys.path.insert(0, str(package_root)) return importlib.import_module("OpenImageIO") - - -def main() -> int: - build_root = pathlib.Path(sys.argv[1]).resolve() - oiio = load_package(build_root) - - print("module:", oiio.__name__) - print("version:", oiio.__version__) - run(oiio) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/testsuite/python-nanobind/ref/out.txt b/testsuite/python-nanobind/ref/out.txt deleted file mode 100644 index 469009902b..0000000000 --- a/testsuite/python-nanobind/ref/out.txt +++ /dev/null @@ -1,46 +0,0 @@ -module: OpenImageIO -version: 3.2.0.1dev -undefined ROI() = -2147483648 0 0 0 0 0 0 0 -r.defined = False -r.nchannels = 0 - -ROI(0, 640, 100, 200) = 0 640 100 200 0 1 0 10000 -ROI(0, 640, 100, 480, 0, 1, 0, 4) = 0 640 0 480 0 1 0 4 -r.xbegin = 0 -r.xend = 640 -r.ybegin = 0 -r.yend = 480 -r.zbegin = 0 -r.zend = 1 -r.chbegin = 0 -r.chend = 4 -r.defined = True -r.width = 640 -r.height = 480 -r.depth = 1 -r.nchannels = 4 -r.npixels = 307200 - -ROI.All = -2147483648 0 0 0 0 0 0 0 - -r == r2 (expect yes): True -r != r2 (expect no): False -r == r3 (expect no): False -r != r3 (expect yes): True - -r contains (10,10) (expect yes): True -r contains (1000,10) (expect no): False -r contains roi(10,20,10,20,0,1,0,1) (expect yes): True -r contains roi(1010,1020,10,20,0,1,0,1) (expect no): False -A = 0 10 0 8 0 1 0 4 -B = 5 15 -1 10 0 1 0 4 -ROI.union(A,B) = 0 15 -1 10 0 1 0 4 -ROI.intersection(A,B) = 5 10 0 8 0 1 0 4 - -Spec's roi is 0 640 0 480 0 1 0 3 -After set, roi is 3 5 7 9 0 1 0 3 -After set, roi_full is 13 15 17 19 0 1 0 3 -r1 = 0 640 0 480 0 1 0 4 -r2 = 42 640 0 480 0 1 0 4 - -Done. diff --git a/testsuite/python-nanobind/run.py b/testsuite/python-roi-nanobind/run.py similarity index 50% rename from testsuite/python-nanobind/run.py rename to testsuite/python-roi-nanobind/run.py index 11b7ca6209..02671fc645 100644 --- a/testsuite/python-nanobind/run.py +++ b/testsuite/python-roi-nanobind/run.py @@ -4,4 +4,5 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO -command += pythonbin + " src/test_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" +refdirlist = [make_relpath(os.path.join(OIIO_TESTSUITE_ROOT, "python-roi", "ref"))] +command += pythonbin + " src/test_roi_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-roi-nanobind/src/test_roi_nanobind.py b/testsuite/python-roi-nanobind/src/test_roi_nanobind.py new file mode 100644 index 0000000000..aebaa82eaa --- /dev/null +++ b/testsuite/python-roi-nanobind/src/test_roi_nanobind.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from __future__ import annotations + +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) +sys.path.insert( + 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-roi" / "src") +) + +from pythonbinding_loaders import load_nanobind_oiio_package # noqa: E402 +from test_roi import run # noqa: E402 + + +def main() -> int: + build_root = pathlib.Path(sys.argv[1]).resolve() + oiio = load_nanobind_oiio_package(build_root) + + run(oiio) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index fba87adff8..b224041bfb 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -69,7 +69,7 @@ def run(oiio): r2.xbegin = 42 print ("r1 =", r1) print ("r2 =", r2) - + print ("") print ("Done.") From 15873c16f5e8d0e7d273c5f6a6229aae58da1c80 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 13:23:07 +1100 Subject: [PATCH 17/26] Add comments to make it clearer Signed-off-by: Aleksandr Motsjonov --- testsuite/common/pythonbinding_loaders.py | 7 +++++++ testsuite/python-roi-nanobind/src/test_roi_nanobind.py | 4 ++++ testsuite/python-roi/src/test_roi.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/testsuite/common/pythonbinding_loaders.py b/testsuite/common/pythonbinding_loaders.py index 986917fdb8..7ac5ad850a 100644 --- a/testsuite/common/pythonbinding_loaders.py +++ b/testsuite/common/pythonbinding_loaders.py @@ -12,6 +12,13 @@ def load_nanobind_oiio_package(build_root: pathlib.Path): + """Import the staged nanobind OpenImageIO package from a build tree. + + The nanobind migration tests do not import an installed Python package. + Instead, they point at the package directory that CMake stages under + ``/lib/python/nanobind`` and temporarily prepend that location + to ``sys.path`` before importing ``OpenImageIO``. + """ package_root = build_root / "lib/python/nanobind" if not (package_root / "OpenImageIO" / "__init__.py").exists(): raise RuntimeError(f"Could not find OpenImageIO package in {package_root}") diff --git a/testsuite/python-roi-nanobind/src/test_roi_nanobind.py b/testsuite/python-roi-nanobind/src/test_roi_nanobind.py index aebaa82eaa..5e9913ba8d 100644 --- a/testsuite/python-roi-nanobind/src/test_roi_nanobind.py +++ b/testsuite/python-roi-nanobind/src/test_roi_nanobind.py @@ -9,6 +9,10 @@ import pathlib import sys +# runtest.py executes this file as a script, not as part of a Python package, +# so relative imports are not available here. Add the ROI test directory and +# shared helper directory to sys.path explicitly, then import the canonical ROI +# test body plus the helper that loads the staged nanobind package from build/. sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) sys.path.insert( 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-roi" / "src") diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index b224041bfb..05acd02efe 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -76,6 +76,9 @@ def run(oiio): def main() -> int: + # Keep the real import-and-execute path in main() so this file still runs + # as the standalone pybind11 ROI test, while the nanobind ROI runner can + # import and reuse run(oiio) without immediately executing the test. import OpenImageIO as oiio try: From 7eece771a50d07282408bec5c70f01eaa808c3b7 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 14 Mar 2026 15:03:58 +1100 Subject: [PATCH 18/26] Fix small issues Signed-off-by: Aleksandr Motsjonov --- INSTALL.md | 10 ++++++---- src/python-nanobind/py_imagespec.cpp | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 19567db3f4..12cf573334 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -42,7 +42,7 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**. * Python >= 3.9 (tested through 3.13). * pybind11 >= 2.7 (tested through 3.0) * NumPy (tested through 2.2.4) - * For the nanobind (WIP) migration backend: + * For the nanobind (WIP) migration backend used in source/CMake builds: * nanobind discoverable by CMake, or installed in the active Python environment so `python -m nanobind --cmake_dir` works * If you want support for PNG files: @@ -161,8 +161,10 @@ Make wrapper (`make PkgName_ROOT=...`). `USE_PYTHON=0` : Omits building the Python bindings. `OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python -binding backend(s) to configure. `both` keeps the existing pybind11 module and -also builds the nanobind (WIP) module. +binding backend(s) to configure for source/CMake builds. `both` keeps the +existing pybind11 module and also builds the nanobind (WIP) module. The +Python packaging path driven by `pyproject.toml` still targets the production +pybind11 bindings today. `OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them unless you are a developer of OIIO or want to verify that your build @@ -254,7 +256,7 @@ Additionally, a few helpful modifiers alter some build-time options: | make USE_QT=0 ... | Skip anything that needs Qt | | make MYCC=xx MYCXX=yy ... | Use custom compilers | | make USE_PYTHON=0 ... | Don't build the Python binding | -| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | Build the existing pybind11 bindings and the nanobind (WIP) module | +| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | For source/CMake builds, build the existing pybind11 bindings and the nanobind (WIP) module | | make BUILD_SHARED_LIBS=0 | Build static library instead of shared | | make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies | | make LINKSTATIC=1 ... | Link with static external libraries when possible | diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp index 0cab3ede3a..6da9278537 100644 --- a/src/python-nanobind/py_imagespec.cpp +++ b/src/python-nanobind/py_imagespec.cpp @@ -9,30 +9,30 @@ namespace { OIIO_NAMESPACE_USING ROI -imagespec_get_roi(const OpenImageIO::v3_1::ImageSpec& spec) +imagespec_get_roi(const ImageSpec& spec) { - return OpenImageIO::v3_1::get_roi(spec); + return get_roi(spec); } ROI -imagespec_get_roi_full(const OpenImageIO::v3_1::ImageSpec& spec) +imagespec_get_roi_full(const ImageSpec& spec) { - return OpenImageIO::v3_1::get_roi_full(spec); + return get_roi_full(spec); } void -imagespec_set_roi(OpenImageIO::v3_1::ImageSpec& spec, const ROI& roi) +imagespec_set_roi(ImageSpec& spec, const ROI& roi) { - OpenImageIO::v3_1::set_roi(spec, roi); + set_roi(spec, roi); } void -imagespec_set_roi_full(OpenImageIO::v3_1::ImageSpec& spec, const ROI& roi) +imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) { - OpenImageIO::v3_1::set_roi_full(spec, roi); + set_roi_full(spec, roi); } } // namespace From e7d291be2f2bb32c8b8db1fcbb1cbf5827fc3db9 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 21:01:46 +1100 Subject: [PATCH 19/26] Add typedesc to nanobind Signed-off-by: Aleksandr Motsjonov --- src/cmake/testing.cmake | 2 +- src/python-nanobind/CMakeLists.txt | 3 +- src/python-nanobind/py_imagespec.cpp | 9 +- src/python-nanobind/py_oiio.cpp | 1 + src/python-nanobind/py_oiio.h | 4 + src/python-nanobind/py_typedesc.cpp | 228 ++++++++++++++++++ testsuite/python-typedesc-nanobind/run.py | 8 + .../src/test_typedesc_nanobind.py | 35 +++ testsuite/python-typedesc/run.py | 1 - .../python-typedesc/src/test_typedesc.py | 41 ++-- 10 files changed, 306 insertions(+), 26 deletions(-) create mode 100644 src/python-nanobind/py_typedesc.cpp create mode 100644 testsuite/python-typedesc-nanobind/run.py create mode 100644 testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 6fb174a98d..7e18a1bcf2 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -234,7 +234,7 @@ macro (oiio_add_all_tests) IMAGEDIR oiio-images ) if (OIIO_BUILD_PYTHON_NANOBIND) - oiio_add_tests (python-roi-nanobind) + oiio_add_tests (python-roi-nanobind python-typedesc-nanobind) endif () endif () diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 42a0e3c94b..44f8ca544d 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -5,7 +5,8 @@ set (nanobind_srcs py_oiio.cpp py_roi.cpp - py_imagespec.cpp) + py_imagespec.cpp + py_typedesc.cpp) set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO) file (MAKE_DIRECTORY ${nanobind_build_package_dir}) diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp index 6da9278537..923300b3d3 100644 --- a/src/python-nanobind/py_imagespec.cpp +++ b/src/python-nanobind/py_imagespec.cpp @@ -49,11 +49,9 @@ declare_imagespec(nb::module_& m) nb::class_(m, "ImageSpec") .def(nb::init<>()) .def("__init__", - [](ImageSpec* self, int xres, int yres, int nchans, int format) { - new (self) - ImageSpec(xres, yres, nchans, - TypeDesc(static_cast( - format))); + [](ImageSpec* self, int xres, int yres, int nchans, + TypeDesc::BASETYPE format) { + new (self) ImageSpec(xres, yres, nchans, TypeDesc(format)); }) .def_rw("x", &ImageSpec::x) .def_rw("y", &ImageSpec::y) @@ -75,7 +73,6 @@ declare_imagespec(nb::module_& m) m.def("get_roi_full", &imagespec_get_roi_full); m.def("set_roi", &imagespec_set_roi); m.def("set_roi_full", &imagespec_set_roi_full); - m.attr("UINT8") = static_cast(TypeDesc::UINT8); } } // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_oiio.cpp b/src/python-nanobind/py_oiio.cpp index 65e4eed3c1..3e258b0e1d 100644 --- a/src/python-nanobind/py_oiio.cpp +++ b/src/python-nanobind/py_oiio.cpp @@ -11,5 +11,6 @@ NB_MODULE(_OpenImageIO, m) PyOpenImageIO::declare_roi(m); PyOpenImageIO::declare_imagespec(m); + PyOpenImageIO::declare_typedesc(m); m.attr("__version__") = OIIO_VERSION_STRING; } diff --git a/src/python-nanobind/py_oiio.h b/src/python-nanobind/py_oiio.h index f4ec3cfe79..e3335397bc 100644 --- a/src/python-nanobind/py_oiio.h +++ b/src/python-nanobind/py_oiio.h @@ -7,10 +7,12 @@ #include #include #include +#include #include #include #include +#include namespace nb = nanobind; using namespace nb::literals; @@ -23,5 +25,7 @@ void declare_roi(nb::module_& m); void declare_imagespec(nb::module_& m); +void +declare_typedesc(nb::module_& m); } // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_typedesc.cpp b/src/python-nanobind/py_typedesc.cpp new file mode 100644 index 0000000000..ba931a340b --- /dev/null +++ b/src/python-nanobind/py_typedesc.cpp @@ -0,0 +1,228 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +template +void +typedesc_property(TypeDesc& t, Enum value); + +template<> +void +typedesc_property(TypeDesc& t, TypeDesc::BASETYPE value) +{ + t.basetype = value; +} + +template<> +void +typedesc_property(TypeDesc& t, TypeDesc::AGGREGATE value) +{ + t.aggregate = value; +} + +template<> +void +typedesc_property(TypeDesc& t, + TypeDesc::VECSEMANTICS value) +{ + t.vecsemantics = value; +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_typedesc(nb::module_& m) +{ + using BASETYPE = TypeDesc::BASETYPE; + using AGGREGATE = TypeDesc::AGGREGATE; + using VECSEMANTICS = TypeDesc::VECSEMANTICS; + + nb::enum_(m, "BASETYPE") + .value("UNKNOWN", TypeDesc::UNKNOWN) + .value("NONE", TypeDesc::NONE) + .value("UCHAR", TypeDesc::UCHAR) + .value("UINT8", TypeDesc::UINT8) + .value("CHAR", TypeDesc::CHAR) + .value("INT8", TypeDesc::INT8) + .value("UINT16", TypeDesc::UINT16) + .value("USHORT", TypeDesc::USHORT) + .value("SHORT", TypeDesc::SHORT) + .value("INT16", TypeDesc::INT16) + .value("UINT", TypeDesc::UINT) + .value("UINT32", TypeDesc::UINT32) + .value("INT", TypeDesc::INT) + .value("INT32", TypeDesc::INT32) + .value("ULONGLONG", TypeDesc::ULONGLONG) + .value("UINT64", TypeDesc::UINT64) + .value("LONGLONG", TypeDesc::LONGLONG) + .value("INT64", TypeDesc::INT64) + .value("HALF", TypeDesc::HALF) + .value("FLOAT", TypeDesc::FLOAT) + .value("DOUBLE", TypeDesc::DOUBLE) + .value("STRING", TypeDesc::STRING) + .value("PTR", TypeDesc::PTR) + .value("LASTBASE", TypeDesc::LASTBASE) + .export_values(); + + nb::enum_(m, "AGGREGATE") + .value("SCALAR", TypeDesc::SCALAR) + .value("VEC2", TypeDesc::VEC2) + .value("VEC3", TypeDesc::VEC3) + .value("VEC4", TypeDesc::VEC4) + .value("MATRIX33", TypeDesc::MATRIX33) + .value("MATRIX44", TypeDesc::MATRIX44) + .export_values(); + + nb::enum_(m, "VECSEMANTICS") + .value("NOXFORM", TypeDesc::NOXFORM) + .value("NOSEMANTICS", TypeDesc::NOSEMANTICS) + .value("COLOR", TypeDesc::COLOR) + .value("POINT", TypeDesc::POINT) + .value("VECTOR", TypeDesc::VECTOR) + .value("NORMAL", TypeDesc::NORMAL) + .value("TIMECODE", TypeDesc::TIMECODE) + .value("KEYCODE", TypeDesc::KEYCODE) + .value("RATIONAL", TypeDesc::RATIONAL) + .value("BOX", TypeDesc::BOX) + .export_values(); + + nb::class_(m, "TypeDesc") + .def_prop_rw( + "basetype", + [](TypeDesc t) { return BASETYPE(t.basetype); }, + [](TypeDesc& t, BASETYPE b) { typedesc_property(t, b); }) + .def_prop_rw( + "aggregate", + [](TypeDesc t) { return AGGREGATE(t.aggregate); }, + [](TypeDesc& t, AGGREGATE b) { typedesc_property(t, b); }) + .def_prop_rw( + "vecsemantics", + [](TypeDesc t) { return VECSEMANTICS(t.vecsemantics); }, + [](TypeDesc& t, VECSEMANTICS b) { typedesc_property(t, b); }) + .def_rw("arraylen", &TypeDesc::arraylen) + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def("c_str", [](const TypeDesc& self) { return std::string(self.c_str()); }) + .def("numelements", &TypeDesc::numelements) + .def("basevalues", &TypeDesc::basevalues) + .def("size", &TypeDesc::size) + .def("elementtype", &TypeDesc::elementtype) + .def("elementsize", &TypeDesc::elementsize) + .def("basesize", &TypeDesc::basesize) + .def("fromstring", + [](TypeDesc& t, const char* typestring) { t.fromstring(typestring); }) + .def("equivalent", &TypeDesc::equivalent) + .def("unarray", &TypeDesc::unarray) + .def("is_vec2", &TypeDesc::is_vec2) + .def("is_vec3", &TypeDesc::is_vec3) + .def("is_vec4", &TypeDesc::is_vec4) + .def("is_box2", &TypeDesc::is_box2) + .def("is_box3", &TypeDesc::is_box3) + .def_static("all_types_equal", + [](const std::vector& types) { + return TypeDesc::all_types_equal(types); + }) + .def(nb::self == nb::self) + .def(nb::self != nb::self) + .def("__str__", [](TypeDesc t) { return std::string(t.c_str()); }) + .def("__repr__", + [](TypeDesc t) { + return Strutil::fmt::format("", t.c_str()); + }); + + m.attr("UNKNOWN") = nb::cast(TypeDesc::UNKNOWN); + m.attr("NONE") = nb::cast(TypeDesc::NONE); + m.attr("UCHAR") = nb::cast(TypeDesc::UCHAR); + m.attr("UINT8") = nb::cast(TypeDesc::UINT8); + m.attr("CHAR") = nb::cast(TypeDesc::CHAR); + m.attr("INT8") = nb::cast(TypeDesc::INT8); + m.attr("UINT16") = nb::cast(TypeDesc::UINT16); + m.attr("USHORT") = nb::cast(TypeDesc::USHORT); + m.attr("SHORT") = nb::cast(TypeDesc::SHORT); + m.attr("INT16") = nb::cast(TypeDesc::INT16); + m.attr("UINT") = nb::cast(TypeDesc::UINT); + m.attr("UINT32") = nb::cast(TypeDesc::UINT32); + m.attr("INT") = nb::cast(TypeDesc::INT); + m.attr("INT32") = nb::cast(TypeDesc::INT32); + m.attr("ULONGLONG") = nb::cast(TypeDesc::ULONGLONG); + m.attr("UINT64") = nb::cast(TypeDesc::UINT64); + m.attr("LONGLONG") = nb::cast(TypeDesc::LONGLONG); + m.attr("INT64") = nb::cast(TypeDesc::INT64); + m.attr("HALF") = nb::cast(TypeDesc::HALF); + m.attr("FLOAT") = nb::cast(TypeDesc::FLOAT); + m.attr("DOUBLE") = nb::cast(TypeDesc::DOUBLE); + m.attr("STRING") = nb::cast(TypeDesc::STRING); + m.attr("PTR") = nb::cast(TypeDesc::PTR); + m.attr("LASTBASE") = nb::cast(TypeDesc::LASTBASE); + + m.attr("SCALAR") = nb::cast(TypeDesc::SCALAR); + m.attr("VEC2") = nb::cast(TypeDesc::VEC2); + m.attr("VEC3") = nb::cast(TypeDesc::VEC3); + m.attr("VEC4") = nb::cast(TypeDesc::VEC4); + m.attr("MATRIX33") = nb::cast(TypeDesc::MATRIX33); + m.attr("MATRIX44") = nb::cast(TypeDesc::MATRIX44); + + m.attr("NOXFORM") = nb::cast(TypeDesc::NOXFORM); + m.attr("NOSEMANTICS") = nb::cast(TypeDesc::NOSEMANTICS); + m.attr("COLOR") = nb::cast(TypeDesc::COLOR); + m.attr("POINT") = nb::cast(TypeDesc::POINT); + m.attr("VECTOR") = nb::cast(TypeDesc::VECTOR); + m.attr("NORMAL") = nb::cast(TypeDesc::NORMAL); + m.attr("TIMECODE") = nb::cast(TypeDesc::TIMECODE); + m.attr("KEYCODE") = nb::cast(TypeDesc::KEYCODE); + m.attr("RATIONAL") = nb::cast(TypeDesc::RATIONAL); + m.attr("BOX") = nb::cast(TypeDesc::BOX); + + m.attr("TypeUnknown") = TypeUnknown; + m.attr("TypeFloat") = TypeFloat; + m.attr("TypeColor") = TypeColor; + m.attr("TypePoint") = TypePoint; + m.attr("TypeVector") = TypeVector; + m.attr("TypeNormal") = TypeNormal; + m.attr("TypeString") = TypeString; + m.attr("TypeInt") = TypeInt; + m.attr("TypeUInt") = TypeUInt; + m.attr("TypeInt64") = TypeInt64; + m.attr("TypeUInt64") = TypeUInt64; + m.attr("TypeInt32") = TypeInt32; + m.attr("TypeUInt32") = TypeUInt32; + m.attr("TypeInt16") = TypeInt16; + m.attr("TypeUInt16") = TypeUInt16; + m.attr("TypeInt8") = TypeInt8; + m.attr("TypeUInt8") = TypeUInt8; + m.attr("TypeHalf") = TypeHalf; + m.attr("TypeMatrix") = TypeMatrix; + m.attr("TypeMatrix33") = TypeMatrix33; + m.attr("TypeMatrix44") = TypeMatrix44; + m.attr("TypeTimeCode") = TypeTimeCode; + m.attr("TypeKeyCode") = TypeKeyCode; + m.attr("TypeFloat2") = TypeFloat2; + m.attr("TypeVector2") = TypeVector2; + m.attr("TypeFloat4") = TypeFloat4; + m.attr("TypeVector4") = TypeVector4; + m.attr("TypeVector2i") = TypeVector2i; + m.attr("TypeVector3i") = TypeVector3i; + m.attr("TypeBox2") = TypeBox2; + m.attr("TypeBox3") = TypeBox3; + m.attr("TypeBox2i") = TypeBox2i; + m.attr("TypeBox3i") = TypeBox3i; + m.attr("TypeRational") = TypeRational; + m.attr("TypeURational") = TypeURational; + m.attr("TypePointer") = TypePointer; +} + +} // namespace PyOpenImageIO diff --git a/testsuite/python-typedesc-nanobind/run.py b/testsuite/python-typedesc-nanobind/run.py new file mode 100644 index 0000000000..60ac856b4f --- /dev/null +++ b/testsuite/python-typedesc-nanobind/run.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +refdirlist = [make_relpath(os.path.join(OIIO_TESTSUITE_ROOT, "python-typedesc", "ref"))] +command += pythonbin + " src/test_typedesc_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py b/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py new file mode 100644 index 0000000000..c666c0a6a2 --- /dev/null +++ b/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from __future__ import annotations + +import pathlib +import sys + +# runtest.py executes this file as a script, not as part of a Python package, +# so relative imports are not available here. Add the TypeDesc test directory +# and shared helper directory to sys.path explicitly, then import the canonical +# TypeDesc test body plus the helper that loads the staged nanobind package +# from build/. +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) +sys.path.insert( + 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-typedesc" / "src") +) + +from pythonbinding_loaders import load_nanobind_oiio_package # noqa: E402 +from test_typedesc import run # noqa: E402 + + +def main() -> int: + build_root = pathlib.Path(sys.argv[1]).resolve() + oiio = load_nanobind_oiio_package(build_root) + + run(oiio) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testsuite/python-typedesc/run.py b/testsuite/python-typedesc/run.py index 180b4a6d01..aa2bf3b55c 100755 --- a/testsuite/python-typedesc/run.py +++ b/testsuite/python-typedesc/run.py @@ -6,4 +6,3 @@ command += pythonbin + " src/test_typedesc.py > out.txt" - diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py index c408b7acb9..9070de6ac4 100755 --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -6,12 +6,8 @@ from __future__ import annotations -import OpenImageIO as oiio - - - # Test that every expected enum value of BASETYPE exists -def basetype_enum_test(): +def basetype_enum_test(oiio): try: oiio.UNKNOWN oiio.NONE @@ -43,7 +39,7 @@ def basetype_enum_test(): # Test that every expected enum value of AGGREGATE exists -def aggregate_enum_test(): +def aggregate_enum_test(oiio): try: oiio.NOSEMANTICS oiio.SCALAR @@ -58,7 +54,7 @@ def aggregate_enum_test(): # Test that every expected enum value of VECSEMANTICS exists -def vecsemantics_enum_test(): +def vecsemantics_enum_test(oiio): try: oiio.NOXFORM oiio.COLOR @@ -74,7 +70,7 @@ def vecsemantics_enum_test(): print ("Failed VECSEMANTICS") # print the details of a type t -def breakdown_test(t: oiio.TypeDesc, name="", verbose=True): +def breakdown_test(t, name="", verbose=True): print ("type '%s'" % name) print (" c_str \"" + t.c_str() + "\"") if verbose: @@ -91,14 +87,11 @@ def breakdown_test(t: oiio.TypeDesc, name="", verbose=True): print (" basesize =", t.basesize()) -###################################################################### -# main test starts here - -try: +def run(oiio): # Test that all the enum values exist - basetype_enum_test() - aggregate_enum_test() - vecsemantics_enum_test() + basetype_enum_test(oiio) + aggregate_enum_test(oiio) + vecsemantics_enum_test(oiio) print ("") # Exercise the different constructors, make sure they create the @@ -177,6 +170,20 @@ def breakdown_test(t: oiio.TypeDesc, name="", verbose=True): print ("") print ("Done.") -except Exception as detail: - print ("Unknown exception:", detail) + +def main() -> int: + # Keep the real import-and-execute path in main() so this file still runs + # as the standalone pybind11 TypeDesc test, while the nanobind TypeDesc + # runner can import and reuse run(oiio) without immediately executing it. + import OpenImageIO as oiio + + try: + run(oiio) + except Exception as detail: + print ("Unknown exception:", detail) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 1585183eda2cd492834e221bd7df8becfb8a3590 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 21:14:53 +1100 Subject: [PATCH 20/26] Add a bit more coverage to both Signed-off-by: Aleksandr Motsjonov --- src/python-nanobind/py_imagespec.cpp | 4 +- src/python-nanobind/py_oiio.cpp | 2 +- src/python-nanobind/py_typedesc.cpp | 48 +++++++++++++------ testsuite/python-roi/ref/out.txt | 4 ++ testsuite/python-roi/src/test_roi.py | 8 ++++ testsuite/python-typedesc/ref/out.txt | 25 ++++++++++ testsuite/python-typedesc/run.py | 2 +- .../python-typedesc/src/test_typedesc.py | 45 +++++++++++++++++ 8 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp index 923300b3d3..a911f087df 100644 --- a/src/python-nanobind/py_imagespec.cpp +++ b/src/python-nanobind/py_imagespec.cpp @@ -50,8 +50,8 @@ declare_imagespec(nb::module_& m) .def(nb::init<>()) .def("__init__", [](ImageSpec* self, int xres, int yres, int nchans, - TypeDesc::BASETYPE format) { - new (self) ImageSpec(xres, yres, nchans, TypeDesc(format)); + const TypeDesc& format) { + new (self) ImageSpec(xres, yres, nchans, format); }) .def_rw("x", &ImageSpec::x) .def_rw("y", &ImageSpec::y) diff --git a/src/python-nanobind/py_oiio.cpp b/src/python-nanobind/py_oiio.cpp index 3e258b0e1d..cded9c62a7 100644 --- a/src/python-nanobind/py_oiio.cpp +++ b/src/python-nanobind/py_oiio.cpp @@ -9,8 +9,8 @@ NB_MODULE(_OpenImageIO, m) { m.doc() = "OpenImageIO nanobind bindings."; + PyOpenImageIO::declare_typedesc(m); PyOpenImageIO::declare_roi(m); PyOpenImageIO::declare_imagespec(m); - PyOpenImageIO::declare_typedesc(m); m.attr("__version__") = OIIO_VERSION_STRING; } diff --git a/src/python-nanobind/py_typedesc.cpp b/src/python-nanobind/py_typedesc.cpp index ba931a340b..7196435839 100644 --- a/src/python-nanobind/py_typedesc.cpp +++ b/src/python-nanobind/py_typedesc.cpp @@ -97,12 +97,10 @@ declare_typedesc(nb::module_& m) nb::class_(m, "TypeDesc") .def_prop_rw( - "basetype", - [](TypeDesc t) { return BASETYPE(t.basetype); }, + "basetype", [](TypeDesc t) { return BASETYPE(t.basetype); }, [](TypeDesc& t, BASETYPE b) { typedesc_property(t, b); }) .def_prop_rw( - "aggregate", - [](TypeDesc t) { return AGGREGATE(t.aggregate); }, + "aggregate", [](TypeDesc t) { return AGGREGATE(t.aggregate); }, [](TypeDesc& t, AGGREGATE b) { typedesc_property(t, b); }) .def_prop_rw( "vecsemantics", @@ -116,7 +114,8 @@ declare_typedesc(nb::module_& m) .def(nb::init()) .def(nb::init()) .def(nb::init()) - .def("c_str", [](const TypeDesc& self) { return std::string(self.c_str()); }) + .def("c_str", + [](const TypeDesc& self) { return std::string(self.c_str()); }) .def("numelements", &TypeDesc::numelements) .def("basevalues", &TypeDesc::basevalues) .def("size", &TypeDesc::size) @@ -124,14 +123,31 @@ declare_typedesc(nb::module_& m) .def("elementsize", &TypeDesc::elementsize) .def("basesize", &TypeDesc::basesize) .def("fromstring", - [](TypeDesc& t, const char* typestring) { t.fromstring(typestring); }) + [](TypeDesc& t, const char* typestring) { + t.fromstring(typestring); + }) .def("equivalent", &TypeDesc::equivalent) .def("unarray", &TypeDesc::unarray) - .def("is_vec2", &TypeDesc::is_vec2) - .def("is_vec3", &TypeDesc::is_vec3) - .def("is_vec4", &TypeDesc::is_vec4) - .def("is_box2", &TypeDesc::is_box2) - .def("is_box3", &TypeDesc::is_box3) + .def("is_vec2", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec2(b); + }) + .def("is_vec3", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec3(b); + }) + .def("is_vec4", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec4(b); + }) + .def("is_box2", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_box2(b); + }) + .def("is_box3", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_box3(b); + }) .def_static("all_types_equal", [](const std::vector& types) { return TypeDesc::all_types_equal(types); @@ -139,10 +155,12 @@ declare_typedesc(nb::module_& m) .def(nb::self == nb::self) .def(nb::self != nb::self) .def("__str__", [](TypeDesc t) { return std::string(t.c_str()); }) - .def("__repr__", - [](TypeDesc t) { - return Strutil::fmt::format("", t.c_str()); - }); + .def("__repr__", [](TypeDesc t) { + return Strutil::fmt::format("", t.c_str()); + }); + + nb::implicitly_convertible(); + nb::implicitly_convertible(); m.attr("UNKNOWN") = nb::cast(TypeDesc::UNKNOWN); m.attr("NONE") = nb::cast(TypeDesc::NONE); diff --git a/testsuite/python-roi/ref/out.txt b/testsuite/python-roi/ref/out.txt index a650c974a2..c3a4e10207 100644 --- a/testsuite/python-roi/ref/out.txt +++ b/testsuite/python-roi/ref/out.txt @@ -30,6 +30,10 @@ r contains (10,10) (expect yes): True r contains (1000,10) (expect no): False r contains roi(10,20,10,20,0,1,0,1) (expect yes): True r contains roi(1010,1020,10,20,0,1,0,1) (expect no): False +ROI(0, 10, 0, 10, 2, 4) = 0 10 0 10 2 4 0 10000 +r5 contains (1,1,2,1) (expect yes): True +r5 contains (1,1,1,1) (expect no): False +r5 contains (1,1,2,3) (expect no): False A = 0 10 0 8 0 1 0 4 B = 5 15 -1 10 0 1 0 4 ROI.union(A,B) = 0 15 -1 10 0 1 0 4 diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 05acd02efe..3eac0492f2 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -48,6 +48,14 @@ def run(oiio): print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) + # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) + # overload with explicit z/channel arguments. + r4 = oiio.ROI (0, 10, 0, 10, 2, 4) + print ("ROI(0, 10, 0, 10, 2, 4) =", r4) + r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) + print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) + print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) + print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) diff --git a/testsuite/python-typedesc/ref/out.txt b/testsuite/python-typedesc/ref/out.txt index 723eb9ba33..0a5f329fe5 100644 --- a/testsuite/python-typedesc/ref/out.txt +++ b/testsuite/python-typedesc/ref/out.txt @@ -159,6 +159,31 @@ equivalent(vector,color) True vector.equivalent(float) False equivalent(vector,float) False +type 'mutated FLOAT, VEC3, COLOR, array of 2' + c_str "color[2]" + basetype BASETYPE.FLOAT + aggregate AGGREGATE.VEC3 + vecsemantics VECSEMANTICS.COLOR + arraylen 2 + str(t) = "color[2]" + size = 24 + elementtype = color + numelements = 2 + basevalues = 6 + elementsize = 12 + basesize = 4 +type 'fromstring('point')' + c_str "point" +after unarray('float[2]') = float +vector is_vec2,is_vec3,is_vec4 = False True False +box2i is_box2,is_box3 = True False +all_types_equal([uint8,uint8]) = True +all_types_equal([uint8,uint16]) = False +repr(TypeFloat) = + +implicit enum ImageSpec roi = 0 8 0 9 0 1 0 3 +implicit str ImageSpec roi = 0 8 0 9 0 1 0 3 + type 'TypeFloat' c_str "float" type 'TypeColor' diff --git a/testsuite/python-typedesc/run.py b/testsuite/python-typedesc/run.py index aa2bf3b55c..0c94900185 100755 --- a/testsuite/python-typedesc/run.py +++ b/testsuite/python-typedesc/run.py @@ -5,4 +5,4 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO -command += pythonbin + " src/test_typedesc.py > out.txt" +command += pythonbin + " src/test_typedesc.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py index 9070de6ac4..8bb1d90129 100755 --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -6,6 +6,9 @@ from __future__ import annotations +import pathlib +import sys + # Test that every expected enum value of BASETYPE exists def basetype_enum_test(oiio): try: @@ -135,6 +138,44 @@ def run(oiio): print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) print ("") + # Exercise property mutation and helper methods that are easy to miss in + # binding ports because they are not just plain constructors/accessors. + t_mut = oiio.TypeDesc() + t_mut.basetype = oiio.FLOAT + t_mut.aggregate = oiio.VEC3 + t_mut.vecsemantics = oiio.COLOR + t_mut.arraylen = 2 + breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") + t_from = oiio.TypeDesc() + t_from.fromstring("point") + breakdown_test (t_from, "fromstring('point')", verbose=False) + t_unarray = oiio.TypeDesc("float[2]") + t_unarray.unarray() + print ("after unarray('float[2]') =", t_unarray) + print ("vector is_vec2,is_vec3,is_vec4 =", + oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) + print ("box2i is_box2,is_box3 =", + oiio.TypeDesc("box2i").is_box2(oiio.INT), + oiio.TypeDesc("box2i").is_box3(oiio.INT)) + print ("all_types_equal([uint8,uint8]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint8")])) + print ("all_types_equal([uint8,uint16]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint16")])) + print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) + print ("") + + # Exercise implicit conversion paths used by the production pybind11 + # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. + implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) + implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") + print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) + print ("implicit str ImageSpec roi =", implicit_str_spec.roi) + print ("") + # Test the pre-constructed types breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) @@ -176,6 +217,10 @@ def main() -> int: # Keep the real import-and-execute path in main() so this file still runs # as the standalone pybind11 TypeDesc test, while the nanobind TypeDesc # runner can import and reuse run(oiio) without immediately executing it. + if len(sys.argv) > 1: + build_root = pathlib.Path(sys.argv[1]).resolve() + sys.path.insert(0, str(build_root / "lib/python/site-packages")) + import OpenImageIO as oiio try: From 3574dd7a47e81701b3380dbaa3a992d662294170 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 22:17:25 +1100 Subject: [PATCH 21/26] Remove extra test folder need entirely. nanobind version is tested automatically when needed. Signed-off-by: Aleksandr Motsjonov --- src/cmake/testing.cmake | 3 -- testsuite/common/pythonbinding_loaders.py | 35 +++++++++++++ testsuite/common/run_nanobind_python_test.py | 34 +++++++++++++ testsuite/python-roi-nanobind/run.py | 8 --- .../src/test_roi_nanobind.py | 34 ------------- testsuite/python-typedesc-nanobind/run.py | 8 --- .../src/test_typedesc_nanobind.py | 35 ------------- testsuite/runtest.py | 50 +++++++++++++++++-- 8 files changed, 114 insertions(+), 93 deletions(-) create mode 100644 testsuite/common/run_nanobind_python_test.py delete mode 100644 testsuite/python-roi-nanobind/run.py delete mode 100644 testsuite/python-roi-nanobind/src/test_roi_nanobind.py delete mode 100644 testsuite/python-typedesc-nanobind/run.py delete mode 100644 testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 7e18a1bcf2..a954a83d21 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -233,9 +233,6 @@ macro (oiio_add_all_tests) python-imageinput python-imagebufalgo IMAGEDIR oiio-images ) - if (OIIO_BUILD_PYTHON_NANOBIND) - oiio_add_tests (python-roi-nanobind python-typedesc-nanobind) - endif () endif () oiio_add_tests (oiiotool-color diff --git a/testsuite/common/pythonbinding_loaders.py b/testsuite/common/pythonbinding_loaders.py index 7ac5ad850a..6c6f1ac1c3 100644 --- a/testsuite/common/pythonbinding_loaders.py +++ b/testsuite/common/pythonbinding_loaders.py @@ -7,6 +7,7 @@ from __future__ import annotations import importlib +import importlib.util import pathlib import sys @@ -25,3 +26,37 @@ def load_nanobind_oiio_package(build_root: pathlib.Path): sys.path.insert(0, str(package_root)) return importlib.import_module("OpenImageIO") + + +def load_python_test_run(test_name: str, script_path: pathlib.Path): + """Load ``run(oiio)`` from a canonical Python testsuite module by path. + + The nanobind migration runners are executed as standalone scripts by + ``runtest.py``, so they cannot rely on package-relative imports. This + helper locates the original pybind11 test module under ``testsuite`` and + returns its exported ``run`` function. + """ + testsuite_root = None + for parent in [script_path.resolve().parent] + list(script_path.resolve().parents): + if parent.name == "testsuite": + testsuite_root = parent + break + if testsuite_root is None: + raise RuntimeError(f"Could not determine testsuite root from {script_path}") + + test_src_dir = testsuite_root / test_name / "src" + test_modules = sorted(test_src_dir.glob("*.py")) + if len(test_modules) != 1: + raise RuntimeError( + f"Expected exactly one Python test module in {test_src_dir}, " + f"found {len(test_modules)}" + ) + test_module = test_modules[0] + spec = importlib.util.spec_from_file_location(f"oiio_{test_name}_module", + test_module) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load Python test module from {test_module}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.run diff --git a/testsuite/common/run_nanobind_python_test.py b/testsuite/common/run_nanobind_python_test.py new file mode 100644 index 0000000000..ca06f31ba2 --- /dev/null +++ b/testsuite/common/run_nanobind_python_test.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from __future__ import annotations + +import pathlib +import sys + +from pythonbinding_loaders import load_nanobind_oiio_package, load_python_test_run + + +def main() -> int: + if len(sys.argv) != 3: + raise SystemExit( + "Usage: run_nanobind_python_test.py " + ) + + test_name = sys.argv[1] + build_root = pathlib.Path(sys.argv[2]).resolve() + run = load_python_test_run(test_name, pathlib.Path(__file__)) + oiio = load_nanobind_oiio_package(build_root) + + try: + run(oiio) + except Exception as detail: + print("Unknown exception:", detail) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testsuite/python-roi-nanobind/run.py b/testsuite/python-roi-nanobind/run.py deleted file mode 100644 index 02671fc645..0000000000 --- a/testsuite/python-roi-nanobind/run.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -refdirlist = [make_relpath(os.path.join(OIIO_TESTSUITE_ROOT, "python-roi", "ref"))] -command += pythonbin + " src/test_roi_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-roi-nanobind/src/test_roi_nanobind.py b/testsuite/python-roi-nanobind/src/test_roi_nanobind.py deleted file mode 100644 index 5e9913ba8d..0000000000 --- a/testsuite/python-roi-nanobind/src/test_roi_nanobind.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from __future__ import annotations - -import pathlib -import sys - -# runtest.py executes this file as a script, not as part of a Python package, -# so relative imports are not available here. Add the ROI test directory and -# shared helper directory to sys.path explicitly, then import the canonical ROI -# test body plus the helper that loads the staged nanobind package from build/. -sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) -sys.path.insert( - 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-roi" / "src") -) - -from pythonbinding_loaders import load_nanobind_oiio_package # noqa: E402 -from test_roi import run # noqa: E402 - - -def main() -> int: - build_root = pathlib.Path(sys.argv[1]).resolve() - oiio = load_nanobind_oiio_package(build_root) - - run(oiio) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/testsuite/python-typedesc-nanobind/run.py b/testsuite/python-typedesc-nanobind/run.py deleted file mode 100644 index 60ac856b4f..0000000000 --- a/testsuite/python-typedesc-nanobind/run.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -refdirlist = [make_relpath(os.path.join(OIIO_TESTSUITE_ROOT, "python-typedesc", "ref"))] -command += pythonbin + " src/test_typedesc_nanobind.py " + OIIO_BUILD_ROOT + " > out.txt" diff --git a/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py b/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py deleted file mode 100644 index c666c0a6a2..0000000000 --- a/testsuite/python-typedesc-nanobind/src/test_typedesc_nanobind.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from __future__ import annotations - -import pathlib -import sys - -# runtest.py executes this file as a script, not as part of a Python package, -# so relative imports are not available here. Add the TypeDesc test directory -# and shared helper directory to sys.path explicitly, then import the canonical -# TypeDesc test body plus the helper that loads the staged nanobind package -# from build/. -sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "common")) -sys.path.insert( - 0, str(pathlib.Path(__file__).resolve().parents[2] / "python-typedesc" / "src") -) - -from pythonbinding_loaders import load_nanobind_oiio_package # noqa: E402 -from test_typedesc import run # noqa: E402 - - -def main() -> int: - build_root = pathlib.Path(sys.argv[1]).resolve() - oiio = load_nanobind_oiio_package(build_root) - - run(oiio) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/testsuite/runtest.py b/testsuite/runtest.py index 052b68434a..62ec2fba08 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -82,6 +82,13 @@ def make_relpath (path: str, start: str=os.curdir) -> str: test_source_dir = os.getenv('OIIO_TESTSUITE_SRC', os.path.join(OIIO_TESTSUITE_ROOT, mytest)) +# Python tests listed here also run against the staged nanobind package when +# it exists in the current build tree. +nanobind_python_tests = { + "python-roi", + "python-typedesc", +} + def oiio_app (app: str) -> str: if (platform.system () != 'Windows' or options.devenv_config == ""): cmd = os.path.join(OIIO_BUILD_ROOT, "bin", app) + " " @@ -101,6 +108,7 @@ def oiio_app (app: str) -> str: command = "" outputs = [ "out.txt" ] # default +ref_name_overrides = {} # The image comparison thresholds are tricky to remember. Here's the key: # A test fails if more than `failpercent` of pixel values differ by more @@ -354,16 +362,18 @@ def oiiotool (args: str, silent: bool=False, concat: bool=True, failureok: bool= # the identical name, and if that fails, it will look for alternatives of # the form "basename-*.ext" (or ANY match in the ref directory, if anymatch # is True). -def checkref (name: str, refdirlist: list[str]) -> tuple[bool, str]: +def checkref (name: str, refdirlist: list[str], refname: str|None=None) -> tuple[bool, str]: # Break the output into prefix+extension - (prefix, extension) = os.path.splitext(name) + if refname is None: + refname = name + (prefix, extension) = os.path.splitext(refname) ok = 0 for ref in refdirlist : # We will first compare name to ref/name, and if that fails, we will # compare it to everything else that matches ref/prefix-*.extension. # That allows us to have multiple matching variants for different # platforms, etc. - defaulttest = os.path.join(ref,name) + defaulttest = os.path.join(ref,refname) if anymatch : pattern = "*.*" else : @@ -437,7 +447,8 @@ def runtest (command: str, outputs: list[str], failureok: int=0) -> int : if os.path.exists('crlf.txt') : os.remove('crlf.txt') - (ok, testfile) = checkref (out, refdirlist) + refname = ref_name_overrides.get(out, out) + (ok, testfile) = checkref (out, refdirlist, refname=refname) if ok : if extension in image_extensions : @@ -477,13 +488,42 @@ def runtest (command: str, outputs: list[str], failureok: int=0) -> int : # -# Read the individual run.py file for this test, which will define +# Read the individual run.py file for this test, which will define # command and outputs. # with open(os.path.join(test_source_dir,"run.py")) as f: code = compile(f.read(), "run.py", 'exec') exec (code) +# For tests that have a nanobind port, run the same canonical Python test +# body a second time against the staged nanobind package from the current +# build tree. Keep the output separate so failures still indicate which +# backend mismatched the shared reference output. +nanobind_package = os.path.join( + OIIO_BUILD_ROOT, "lib", "python", "nanobind", "OpenImageIO", "__init__.py" +) +if mytest in nanobind_python_tests and os.path.exists(nanobind_package): + nanobind_runner = make_relpath( + os.path.join(OIIO_TESTSUITE_ROOT, "common", "run_nanobind_python_test.py"), + tmpdir, + ) + command += " ; " + ( + pythonbin + + " " + + nanobind_runner + + " " + + mytest + + " " + + OIIO_BUILD_ROOT + + " > out-nanobind.txt" + ) + # Example of final command for `python-roi` would be: + # python src/test_roi.py > out.txt ; \ + # python ../../../testsuite/common/run_nanobind_python_test.py \ + # python-roi ../.. > out-nanobind.txt + outputs.append("out-nanobind.txt") + ref_name_overrides["out-nanobind.txt"] = "out.txt" + # Allow a little more slop for slight pixel differences when in DEBUG # mode or when running on remote CI machines. if (os.getenv('CI') or os.getenv('DEBUG')) : From 576cb199094b623692219ef61325f7bcaa663dba Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 22:31:52 +1100 Subject: [PATCH 22/26] minimize need for changes even more Signed-off-by: Aleksandr Motsjonov --- testsuite/common/pythonbinding_loaders.py | 62 ----- testsuite/common/run_nanobind_python_test.py | 34 --- testsuite/python-roi/src/test_roi.py | 154 ++++++----- .../python-typedesc/src/test_typedesc.py | 249 +++++++++--------- testsuite/runtest.py | 53 +++- 5 files changed, 237 insertions(+), 315 deletions(-) delete mode 100644 testsuite/common/pythonbinding_loaders.py delete mode 100644 testsuite/common/run_nanobind_python_test.py mode change 100755 => 100644 testsuite/python-typedesc/src/test_typedesc.py diff --git a/testsuite/common/pythonbinding_loaders.py b/testsuite/common/pythonbinding_loaders.py deleted file mode 100644 index 6c6f1ac1c3..0000000000 --- a/testsuite/common/pythonbinding_loaders.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from __future__ import annotations - -import importlib -import importlib.util -import pathlib -import sys - - -def load_nanobind_oiio_package(build_root: pathlib.Path): - """Import the staged nanobind OpenImageIO package from a build tree. - - The nanobind migration tests do not import an installed Python package. - Instead, they point at the package directory that CMake stages under - ``/lib/python/nanobind`` and temporarily prepend that location - to ``sys.path`` before importing ``OpenImageIO``. - """ - package_root = build_root / "lib/python/nanobind" - if not (package_root / "OpenImageIO" / "__init__.py").exists(): - raise RuntimeError(f"Could not find OpenImageIO package in {package_root}") - - sys.path.insert(0, str(package_root)) - return importlib.import_module("OpenImageIO") - - -def load_python_test_run(test_name: str, script_path: pathlib.Path): - """Load ``run(oiio)`` from a canonical Python testsuite module by path. - - The nanobind migration runners are executed as standalone scripts by - ``runtest.py``, so they cannot rely on package-relative imports. This - helper locates the original pybind11 test module under ``testsuite`` and - returns its exported ``run`` function. - """ - testsuite_root = None - for parent in [script_path.resolve().parent] + list(script_path.resolve().parents): - if parent.name == "testsuite": - testsuite_root = parent - break - if testsuite_root is None: - raise RuntimeError(f"Could not determine testsuite root from {script_path}") - - test_src_dir = testsuite_root / test_name / "src" - test_modules = sorted(test_src_dir.glob("*.py")) - if len(test_modules) != 1: - raise RuntimeError( - f"Expected exactly one Python test module in {test_src_dir}, " - f"found {len(test_modules)}" - ) - test_module = test_modules[0] - spec = importlib.util.spec_from_file_location(f"oiio_{test_name}_module", - test_module) - if spec is None or spec.loader is None: - raise RuntimeError(f"Could not load Python test module from {test_module}") - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module.run diff --git a/testsuite/common/run_nanobind_python_test.py b/testsuite/common/run_nanobind_python_test.py deleted file mode 100644 index ca06f31ba2..0000000000 --- a/testsuite/common/run_nanobind_python_test.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -# Copyright Contributors to the OpenImageIO project. -# SPDX-License-Identifier: Apache-2.0 -# https://github.com/AcademySoftwareFoundation/OpenImageIO - -from __future__ import annotations - -import pathlib -import sys - -from pythonbinding_loaders import load_nanobind_oiio_package, load_python_test_run - - -def main() -> int: - if len(sys.argv) != 3: - raise SystemExit( - "Usage: run_nanobind_python_test.py " - ) - - test_name = sys.argv[1] - build_root = pathlib.Path(sys.argv[2]).resolve() - run = load_python_test_run(test_name, pathlib.Path(__file__)) - oiio = load_nanobind_oiio_package(build_root) - - try: - run(oiio) - except Exception as detail: - print("Unknown exception:", detail) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 3eac0492f2..c7edad7481 100644 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -6,91 +6,85 @@ from __future__ import annotations -def run(oiio): - r = oiio.ROI() - print ("undefined ROI() =", r) - print ("r.defined =", r.defined) - print ("r.nchannels =", r.nchannels) - print ("") - - r = oiio.ROI (0, 640, 100, 200) - print ("ROI(0, 640, 100, 200) =", r) - r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) - print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) - print ("r.xbegin =", r.xbegin) - print ("r.xend =", r.xend) - print ("r.ybegin =", r.ybegin) - print ("r.yend =", r.yend) - print ("r.zbegin =", r.zbegin) - print ("r.zend =", r.zend) - print ("r.chbegin =", r.chbegin) - print ("r.chend =", r.chend) - print ("r.defined = ", r.defined) - print ("r.width = ", r.width) - print ("r.height = ", r.height) - print ("r.depth = ", r.depth) - print ("r.nchannels = ", r.nchannels) - print ("r.npixels = ", r.npixels) - print ("") - print ("ROI.All =", oiio.ROI.All) - print ("") - - r2 = oiio.ROI(r) - r3 = oiio.ROI(r) - r3.xend = 320 - print ("r == r2 (expect yes): ", (r == r2)) - print ("r != r2 (expect no): ", (r != r2)) - print ("r == r3 (expect no): ", (r == r3)) - print ("r != r3 (expect yes): ", (r != r3)) - print ("") - - print ("r contains (10,10) (expect yes): ", r.contains(10,10)) - print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) - print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) - print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) - # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) - # overload with explicit z/channel arguments. - r4 = oiio.ROI (0, 10, 0, 10, 2, 4) - print ("ROI(0, 10, 0, 10, 2, 4) =", r4) - r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) - print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) - print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) - print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) - - A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) - B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) - print ("A =", A) - print ("B =", B) - print ("ROI.union(A,B) =", oiio.union(A,B)) - print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) - print ("") - - spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) - print ("Spec's roi is", oiio.get_roi(spec)) - oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) - oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) - print ("After set, roi is", oiio.get_roi(spec)) - print ("After set, roi_full is", oiio.get_roi_full(spec)) - - r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - r2 = r1.copy() - r2.xbegin = 42 - print ("r1 =", r1) - print ("r2 =", r2) - - print ("") - - print ("Done.") - def main() -> int: - # Keep the real import-and-execute path in main() so this file still runs - # as the standalone pybind11 ROI test, while the nanobind ROI runner can - # import and reuse run(oiio) without immediately executing the test. import OpenImageIO as oiio try: - run(oiio) + r = oiio.ROI() + print ("undefined ROI() =", r) + print ("r.defined =", r.defined) + print ("r.nchannels =", r.nchannels) + print ("") + + r = oiio.ROI (0, 640, 100, 200) + print ("ROI(0, 640, 100, 200) =", r) + r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) + print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) + print ("r.xbegin =", r.xbegin) + print ("r.xend =", r.xend) + print ("r.ybegin =", r.ybegin) + print ("r.yend =", r.yend) + print ("r.zbegin =", r.zbegin) + print ("r.zend =", r.zend) + print ("r.chbegin =", r.chbegin) + print ("r.chend =", r.chend) + print ("r.defined = ", r.defined) + print ("r.width = ", r.width) + print ("r.height = ", r.height) + print ("r.depth = ", r.depth) + print ("r.nchannels = ", r.nchannels) + print ("r.npixels = ", r.npixels) + print ("") + print ("ROI.All =", oiio.ROI.All) + print ("") + + r2 = oiio.ROI(r) + r3 = oiio.ROI(r) + r3.xend = 320 + print ("r == r2 (expect yes): ", (r == r2)) + print ("r != r2 (expect no): ", (r != r2)) + print ("r == r3 (expect no): ", (r == r3)) + print ("r != r3 (expect yes): ", (r != r3)) + print ("") + + print ("r contains (10,10) (expect yes): ", r.contains(10,10)) + print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) + print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) + print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) + # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) + # overload with explicit z/channel arguments. + r4 = oiio.ROI (0, 10, 0, 10, 2, 4) + print ("ROI(0, 10, 0, 10, 2, 4) =", r4) + r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) + print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) + print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) + print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) + + A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) + B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) + print ("A =", A) + print ("B =", B) + print ("ROI.union(A,B) =", oiio.union(A,B)) + print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) + print ("") + + spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) + print ("Spec's roi is", oiio.get_roi(spec)) + oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) + oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) + print ("After set, roi is", oiio.get_roi(spec)) + print ("After set, roi_full is", oiio.get_roi_full(spec)) + + r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + r2 = r1.copy() + r2.xbegin = 42 + print ("r1 =", r1) + print ("r2 =", r2) + + print ("") + + print ("Done.") except Exception as detail: print ("Unknown exception:", detail) return 0 diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py old mode 100755 new mode 100644 index 8bb1d90129..4d283ba16e --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -9,6 +9,7 @@ import pathlib import sys + # Test that every expected enum value of BASETYPE exists def basetype_enum_test(oiio): try: @@ -72,6 +73,7 @@ def vecsemantics_enum_test(oiio): except: print ("Failed VECSEMANTICS") + # print the details of a type t def breakdown_test(t, name="", verbose=True): print ("type '%s'" % name) @@ -90,133 +92,7 @@ def breakdown_test(t, name="", verbose=True): print (" basesize =", t.basesize()) -def run(oiio): - # Test that all the enum values exist - basetype_enum_test(oiio) - aggregate_enum_test(oiio) - vecsemantics_enum_test(oiio) - print ("") - - # Exercise the different constructors, make sure they create the - # correct TypeDesc (also exercises the individual fields, c_str(), - # conversion to string). - breakdown_test (oiio.TypeDesc(), "(default)") - breakdown_test (oiio.TypeDesc(oiio.UINT8), "UINT8") - breakdown_test (oiio.TypeDesc(oiio.HALF, oiio.VEC3, oiio.COLOR), - "HALF, VEC3, COLOR") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.SCALAR, oiio.NOXFORM, 6), - "FLOAT, SCALAR, NOXFORM, array of 6") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.POINT, 2), - "FLOAT, VEC3, POINT, array of 2") - breakdown_test (oiio.TypeDesc(oiio.INT, oiio.VEC2, oiio.BOX, 2), - "INT, VEC2, BOX, array of 2") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.BOX, 2), - "FLOAT, VEC3, BOX, array of 2") - print ("") - - # Test construction from a string descriptor - breakdown_test (oiio.TypeDesc("float[2]"), "float[2]") - breakdown_test (oiio.TypeDesc("normal"), "normal") - breakdown_test (oiio.TypeDesc("uint16"), "uint16") - breakdown_test (oiio.TypeDesc("box3"), "box3") - print ("") - - # Test equality, inequality, and equivalent - t_uint8 = oiio.TypeDesc("uint8") - t_uint16 = oiio.TypeDesc("uint16") - t_uint8_b = oiio.TypeDesc("uint8") - print ("uint8 == uint8?", (t_uint8 == t_uint8)) - print ("uint8 == uint8?", (t_uint8 == t_uint8_b)) - print ("uint8 == uint16", (t_uint8 == t_uint16)) - print ("uint8 != uint8?", (t_uint8 != t_uint8)) - print ("uint8 != uint8?", (t_uint8 != t_uint8_b)) - print ("uint8 != uint16", (t_uint8 != t_uint16)) - print ("vector == color", (oiio.TypeDesc("vector") == oiio.TypeDesc("color"))) - print ("vector.equivalent(color)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("color"))) - print ("equivalent(vector,color)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("color"))) - print ("vector.equivalent(float)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("float"))) - print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) - print ("") - - # Exercise property mutation and helper methods that are easy to miss in - # binding ports because they are not just plain constructors/accessors. - t_mut = oiio.TypeDesc() - t_mut.basetype = oiio.FLOAT - t_mut.aggregate = oiio.VEC3 - t_mut.vecsemantics = oiio.COLOR - t_mut.arraylen = 2 - breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") - t_from = oiio.TypeDesc() - t_from.fromstring("point") - breakdown_test (t_from, "fromstring('point')", verbose=False) - t_unarray = oiio.TypeDesc("float[2]") - t_unarray.unarray() - print ("after unarray('float[2]') =", t_unarray) - print ("vector is_vec2,is_vec3,is_vec4 =", - oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), - oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), - oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) - print ("box2i is_box2,is_box3 =", - oiio.TypeDesc("box2i").is_box2(oiio.INT), - oiio.TypeDesc("box2i").is_box3(oiio.INT)) - print ("all_types_equal([uint8,uint8]) =", - oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), - oiio.TypeDesc("uint8")])) - print ("all_types_equal([uint8,uint16]) =", - oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), - oiio.TypeDesc("uint16")])) - print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) - print ("") - - # Exercise implicit conversion paths used by the production pybind11 - # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. - implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) - implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") - print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) - print ("implicit str ImageSpec roi =", implicit_str_spec.roi) - print ("") - - # Test the pre-constructed types - breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) - breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) - breakdown_test (oiio.TypeString, "TypeString", verbose=False) - breakdown_test (oiio.TypeInt, "TypeInt", verbose=False) - breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) - breakdown_test (oiio.TypeInt64, "TypeInt64", verbose=False) - breakdown_test (oiio.TypeUInt64, "TypeUInt64", verbose=False) - breakdown_test (oiio.TypeInt32, "TypeInt32", verbose=False) - breakdown_test (oiio.TypeUInt32, "TypeUInt32", verbose=False) - breakdown_test (oiio.TypeInt16, "TypeInt16", verbose=False) - breakdown_test (oiio.TypeUInt16, "TypeUInt16", verbose=False) - breakdown_test (oiio.TypeInt8, "TypeInt8", verbose=False) - breakdown_test (oiio.TypeUInt8, "TypeUInt8", verbose=False) - breakdown_test (oiio.TypePoint, "TypePoint", verbose=False) - breakdown_test (oiio.TypeVector, "TypeVector", verbose=False) - breakdown_test (oiio.TypeNormal, "TypeNormal", verbose=False) - breakdown_test (oiio.TypeMatrix, "TypeMatrix", verbose=False) - breakdown_test (oiio.TypeMatrix33, "TypeMatrix33", verbose=False) - breakdown_test (oiio.TypeMatrix44, "TypeMatrix44", verbose=False) - breakdown_test (oiio.TypeTimeCode, "TypeTimeCode", verbose=False) - breakdown_test (oiio.TypeKeyCode, "TypeKeyCode", verbose=False) - breakdown_test (oiio.TypeFloat2, "TypeFloat2", verbose=False) - breakdown_test (oiio.TypeVector2, "TypeVector2", verbose=False) - breakdown_test (oiio.TypeFloat4, "TypeFloat4", verbose=False) - breakdown_test (oiio.TypeVector4, "TypeVector4", verbose=False) - breakdown_test (oiio.TypeVector2i, "TypeVector2i", verbose=False) - breakdown_test (oiio.TypeVector3i, "TypeVector3i", verbose=False) - breakdown_test (oiio.TypeHalf, "TypeHalf", verbose=False) - breakdown_test (oiio.TypeRational, "TypeRational", verbose=False) - breakdown_test (oiio.TypeURational, "TypeURational", verbose=False) - breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) - print ("") - - print ("Done.") - - def main() -> int: - # Keep the real import-and-execute path in main() so this file still runs - # as the standalone pybind11 TypeDesc test, while the nanobind TypeDesc - # runner can import and reuse run(oiio) without immediately executing it. if len(sys.argv) > 1: build_root = pathlib.Path(sys.argv[1]).resolve() sys.path.insert(0, str(build_root / "lib/python/site-packages")) @@ -224,7 +100,126 @@ def main() -> int: import OpenImageIO as oiio try: - run(oiio) + # Test that all the enum values exist + basetype_enum_test(oiio) + aggregate_enum_test(oiio) + vecsemantics_enum_test(oiio) + print ("") + + # Exercise the different constructors, make sure they create the + # correct TypeDesc (also exercises the individual fields, c_str(), + # conversion to string). + breakdown_test (oiio.TypeDesc(), "(default)") + breakdown_test (oiio.TypeDesc(oiio.UINT8), "UINT8") + breakdown_test (oiio.TypeDesc(oiio.HALF, oiio.VEC3, oiio.COLOR), + "HALF, VEC3, COLOR") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.SCALAR, oiio.NOXFORM, 6), + "FLOAT, SCALAR, NOXFORM, array of 6") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.POINT, 2), + "FLOAT, VEC3, POINT, array of 2") + breakdown_test (oiio.TypeDesc(oiio.INT, oiio.VEC2, oiio.BOX, 2), + "INT, VEC2, BOX, array of 2") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.BOX, 2), + "FLOAT, VEC3, BOX, array of 2") + print ("") + + # Test construction from a string descriptor + breakdown_test (oiio.TypeDesc("float[2]"), "float[2]") + breakdown_test (oiio.TypeDesc("normal"), "normal") + breakdown_test (oiio.TypeDesc("uint16"), "uint16") + breakdown_test (oiio.TypeDesc("box3"), "box3") + print ("") + + # Test equality, inequality, and equivalent + t_uint8 = oiio.TypeDesc("uint8") + t_uint16 = oiio.TypeDesc("uint16") + t_uint8_b = oiio.TypeDesc("uint8") + print ("uint8 == uint8?", (t_uint8 == t_uint8)) + print ("uint8 == uint8?", (t_uint8 == t_uint8_b)) + print ("uint8 == uint16", (t_uint8 == t_uint16)) + print ("uint8 != uint8?", (t_uint8 != t_uint8)) + print ("uint8 != uint8?", (t_uint8 != t_uint8_b)) + print ("uint8 != uint16", (t_uint8 != t_uint16)) + print ("vector == color", (oiio.TypeDesc("vector") == oiio.TypeDesc("color"))) + print ("vector.equivalent(color)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("color"))) + print ("equivalent(vector,color)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("color"))) + print ("vector.equivalent(float)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("float"))) + print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) + print ("") + + # Exercise property mutation and helper methods that are easy to miss in + # binding ports because they are not just plain constructors/accessors. + t_mut = oiio.TypeDesc() + t_mut.basetype = oiio.FLOAT + t_mut.aggregate = oiio.VEC3 + t_mut.vecsemantics = oiio.COLOR + t_mut.arraylen = 2 + breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") + t_from = oiio.TypeDesc() + t_from.fromstring("point") + breakdown_test (t_from, "fromstring('point')", verbose=False) + t_unarray = oiio.TypeDesc("float[2]") + t_unarray.unarray() + print ("after unarray('float[2]') =", t_unarray) + print ("vector is_vec2,is_vec3,is_vec4 =", + oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) + print ("box2i is_box2,is_box3 =", + oiio.TypeDesc("box2i").is_box2(oiio.INT), + oiio.TypeDesc("box2i").is_box3(oiio.INT)) + print ("all_types_equal([uint8,uint8]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint8")])) + print ("all_types_equal([uint8,uint16]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint16")])) + print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) + print ("") + + # Exercise implicit conversion paths used by the production pybind11 + # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. + implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) + implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") + print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) + print ("implicit str ImageSpec roi =", implicit_str_spec.roi) + print ("") + + # Test the pre-constructed types + breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) + breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) + breakdown_test (oiio.TypeString, "TypeString", verbose=False) + breakdown_test (oiio.TypeInt, "TypeInt", verbose=False) + breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) + breakdown_test (oiio.TypeInt64, "TypeInt64", verbose=False) + breakdown_test (oiio.TypeUInt64, "TypeUInt64", verbose=False) + breakdown_test (oiio.TypeInt32, "TypeInt32", verbose=False) + breakdown_test (oiio.TypeUInt32, "TypeUInt32", verbose=False) + breakdown_test (oiio.TypeInt16, "TypeInt16", verbose=False) + breakdown_test (oiio.TypeUInt16, "TypeUInt16", verbose=False) + breakdown_test (oiio.TypeInt8, "TypeInt8", verbose=False) + breakdown_test (oiio.TypeUInt8, "TypeUInt8", verbose=False) + breakdown_test (oiio.TypePoint, "TypePoint", verbose=False) + breakdown_test (oiio.TypeVector, "TypeVector", verbose=False) + breakdown_test (oiio.TypeNormal, "TypeNormal", verbose=False) + breakdown_test (oiio.TypeMatrix, "TypeMatrix", verbose=False) + breakdown_test (oiio.TypeMatrix33, "TypeMatrix33", verbose=False) + breakdown_test (oiio.TypeMatrix44, "TypeMatrix44", verbose=False) + breakdown_test (oiio.TypeTimeCode, "TypeTimeCode", verbose=False) + breakdown_test (oiio.TypeKeyCode, "TypeKeyCode", verbose=False) + breakdown_test (oiio.TypeFloat2, "TypeFloat2", verbose=False) + breakdown_test (oiio.TypeVector2, "TypeVector2", verbose=False) + breakdown_test (oiio.TypeFloat4, "TypeFloat4", verbose=False) + breakdown_test (oiio.TypeVector4, "TypeVector4", verbose=False) + breakdown_test (oiio.TypeVector2i, "TypeVector2i", verbose=False) + breakdown_test (oiio.TypeVector3i, "TypeVector3i", verbose=False) + breakdown_test (oiio.TypeHalf, "TypeHalf", verbose=False) + breakdown_test (oiio.TypeRational, "TypeRational", verbose=False) + breakdown_test (oiio.TypeURational, "TypeURational", verbose=False) + breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) + print ("") + + print ("Done.") except Exception as detail: print ("Unknown exception:", detail) return 0 diff --git a/testsuite/runtest.py b/testsuite/runtest.py index 62ec2fba08..fcd8b7f001 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -12,6 +12,7 @@ import difflib import filecmp import shutil +import shlex from optparse import OptionParser @@ -53,6 +54,12 @@ def make_relpath (path: str, start: str=os.curdir) -> str: p = os.path.relpath (path, start) return p if platform.system() != 'Windows' else p.replace ('\\', '/') + +def shell_quote(arg: str) -> str: + if platform.system() == 'Windows': + return subprocess.list2cmdline([arg]) + return shlex.quote(arg) + # Try to figure out where some key things are. Go by env variables set by # the cmake tests, but if those aren't set, assume somebody is running # this script by hand from inside build/testsuite/TEST and that @@ -410,6 +417,9 @@ def runtest (command: str, outputs: list[str], failureok: int=0) -> int : err = 0 # print ("working dir = " + tmpdir) os.chdir (srcdir) + for out in outputs: + if os.path.exists(out): + os.remove(out) open ("out.txt", "w").close() # truncate out.txt open ("out.err.txt", "w").close() # truncate out.txt if os.path.isfile("debug.log") : @@ -503,24 +513,43 @@ def runtest (command: str, outputs: list[str], failureok: int=0) -> int : OIIO_BUILD_ROOT, "lib", "python", "nanobind", "OpenImageIO", "__init__.py" ) if mytest in nanobind_python_tests and os.path.exists(nanobind_package): - nanobind_runner = make_relpath( - os.path.join(OIIO_TESTSUITE_ROOT, "common", "run_nanobind_python_test.py"), + nanobind_test_scripts = sorted(glob.glob(os.path.join(test_source_dir, "src", "*.py"))) + if len(nanobind_test_scripts) != 1: + raise RuntimeError( + "Expected exactly one Python test script under " + + os.path.join(test_source_dir, "src") + + ", found " + + str(len(nanobind_test_scripts)) + ) + nanobind_test_script = make_relpath(nanobind_test_scripts[0], tmpdir) + nanobind_package_root = make_relpath( + os.path.join(OIIO_BUILD_ROOT, "lib", "python", "nanobind"), tmpdir, ) - command += " ; " + ( - pythonbin - + " " - + nanobind_runner - + " " - + mytest - + " " - + OIIO_BUILD_ROOT + # Re-run the same canonical test script as a standalone program, but + # with the staged nanobind package inserted first on sys.path so + # `import OpenImageIO` resolves to the nanobind backend. + nanobind_code = ( + "import runpy, sys\n" + + "sys.path.insert(0, " + + repr(nanobind_package_root) + + ")\n" + + "runpy.run_path(" + + repr(nanobind_test_script) + + ", run_name='__main__')\n" + ) + command += ( + " ; " + + shell_quote(pythonbin) + + " -c " + + shell_quote(nanobind_code) + " > out-nanobind.txt" ) # Example of final command for `python-roi` would be: # python src/test_roi.py > out.txt ; \ - # python ../../../testsuite/common/run_nanobind_python_test.py \ - # python-roi ../.. > out-nanobind.txt + # python -c 'import runpy, sys + # sys.path.insert(0, "../../lib/python/nanobind") + # runpy.run_path("src/test_roi.py", run_name="__main__")' > out-nanobind.txt outputs.append("out-nanobind.txt") ref_name_overrides["out-nanobind.txt"] = "out.txt" From 190fff1ff9cff07119ab61b854cdaeccaa15242e Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 22:38:50 +1100 Subject: [PATCH 23/26] less changes Signed-off-by: Aleksandr Motsjonov --- testsuite/python-roi/src/test_roi.py | 146 +++++----- .../python-typedesc/src/test_typedesc.py | 265 +++++++++--------- 2 files changed, 199 insertions(+), 212 deletions(-) mode change 100644 => 100755 testsuite/python-roi/src/test_roi.py mode change 100644 => 100755 testsuite/python-typedesc/src/test_typedesc.py diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py old mode 100644 new mode 100755 index c7edad7481..dbf99152b3 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -6,89 +6,83 @@ from __future__ import annotations +import OpenImageIO as oiio -def main() -> int: - import OpenImageIO as oiio - try: - r = oiio.ROI() - print ("undefined ROI() =", r) - print ("r.defined =", r.defined) - print ("r.nchannels =", r.nchannels) - print ("") +try: + r = oiio.ROI() + print ("undefined ROI() =", r) + print ("r.defined =", r.defined) + print ("r.nchannels =", r.nchannels) + print ("") - r = oiio.ROI (0, 640, 100, 200) - print ("ROI(0, 640, 100, 200) =", r) - r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) - print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) - print ("r.xbegin =", r.xbegin) - print ("r.xend =", r.xend) - print ("r.ybegin =", r.ybegin) - print ("r.yend =", r.yend) - print ("r.zbegin =", r.zbegin) - print ("r.zend =", r.zend) - print ("r.chbegin =", r.chbegin) - print ("r.chend =", r.chend) - print ("r.defined = ", r.defined) - print ("r.width = ", r.width) - print ("r.height = ", r.height) - print ("r.depth = ", r.depth) - print ("r.nchannels = ", r.nchannels) - print ("r.npixels = ", r.npixels) - print ("") - print ("ROI.All =", oiio.ROI.All) - print ("") + r = oiio.ROI (0, 640, 100, 200) + print ("ROI(0, 640, 100, 200) =", r) + r = oiio.ROI (0, 640, 0, 480, 0, 1, 0, 4) + print ("ROI(0, 640, 100, 480, 0, 1, 0, 4) =", r) + print ("r.xbegin =", r.xbegin) + print ("r.xend =", r.xend) + print ("r.ybegin =", r.ybegin) + print ("r.yend =", r.yend) + print ("r.zbegin =", r.zbegin) + print ("r.zend =", r.zend) + print ("r.chbegin =", r.chbegin) + print ("r.chend =", r.chend) + print ("r.defined = ", r.defined) + print ("r.width = ", r.width) + print ("r.height = ", r.height) + print ("r.depth = ", r.depth) + print ("r.nchannels = ", r.nchannels) + print ("r.npixels = ", r.npixels) + print ("") + print ("ROI.All =", oiio.ROI.All) + print ("") - r2 = oiio.ROI(r) - r3 = oiio.ROI(r) - r3.xend = 320 - print ("r == r2 (expect yes): ", (r == r2)) - print ("r != r2 (expect no): ", (r != r2)) - print ("r == r3 (expect no): ", (r == r3)) - print ("r != r3 (expect yes): ", (r != r3)) - print ("") + r2 = oiio.ROI(r) + r3 = oiio.ROI(r) + r3.xend = 320 + print ("r == r2 (expect yes): ", (r == r2)) + print ("r != r2 (expect no): ", (r != r2)) + print ("r == r3 (expect no): ", (r == r3)) + print ("r != r3 (expect yes): ", (r != r3)) + print ("") - print ("r contains (10,10) (expect yes): ", r.contains(10,10)) - print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) - print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) - print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) - # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) - # overload with explicit z/channel arguments. - r4 = oiio.ROI (0, 10, 0, 10, 2, 4) - print ("ROI(0, 10, 0, 10, 2, 4) =", r4) - r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) - print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) - print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) - print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) + print ("r contains (10,10) (expect yes): ", r.contains(10,10)) + print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) + print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) + print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) + # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) + # overload with explicit z/channel arguments. + r4 = oiio.ROI (0, 10, 0, 10, 2, 4) + print ("ROI(0, 10, 0, 10, 2, 4) =", r4) + r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) + print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) + print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) + print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) - A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) - B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) - print ("A =", A) - print ("B =", B) - print ("ROI.union(A,B) =", oiio.union(A,B)) - print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) - print ("") + A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) + B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) + print ("A =", A) + print ("B =", B) + print ("ROI.union(A,B) =", oiio.union(A,B)) + print ("ROI.intersection(A,B) =", oiio.intersection(A,B)) + print ("") - spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) - print ("Spec's roi is", oiio.get_roi(spec)) - oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) - oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) - print ("After set, roi is", oiio.get_roi(spec)) - print ("After set, roi_full is", oiio.get_roi_full(spec)) + spec = oiio.ImageSpec(640, 480, 3, oiio.UINT8) + print ("Spec's roi is", oiio.get_roi(spec)) + oiio.set_roi (spec, oiio.ROI(3, 5, 7, 9)) + oiio.set_roi_full (spec, oiio.ROI(13, 15, 17, 19)) + print ("After set, roi is", oiio.get_roi(spec)) + print ("After set, roi_full is", oiio.get_roi_full(spec)) - r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) - r2 = r1.copy() - r2.xbegin = 42 - print ("r1 =", r1) - print ("r2 =", r2) + r1 = oiio.ROI(0, 640, 0, 480, 0, 1, 0, 4) + r2 = r1.copy() + r2.xbegin = 42 + print ("r1 =", r1) + print ("r2 =", r2) - print ("") + print ("") - print ("Done.") - except Exception as detail: - print ("Unknown exception:", detail) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) + print ("Done.") +except Exception as detail: + print ("Unknown exception:", detail) diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py old mode 100644 new mode 100755 index 4d283ba16e..2a3a74c1fe --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -9,6 +9,12 @@ import pathlib import sys +if len(sys.argv) > 1: + build_root = pathlib.Path(sys.argv[1]).resolve() + sys.path.insert(0, str(build_root / "lib/python/site-packages")) + +import OpenImageIO as oiio + # Test that every expected enum value of BASETYPE exists def basetype_enum_test(oiio): @@ -91,139 +97,126 @@ def breakdown_test(t, name="", verbose=True): print (" elementsize =", t.elementsize()) print (" basesize =", t.basesize()) - -def main() -> int: - if len(sys.argv) > 1: - build_root = pathlib.Path(sys.argv[1]).resolve() - sys.path.insert(0, str(build_root / "lib/python/site-packages")) - - import OpenImageIO as oiio - - try: - # Test that all the enum values exist - basetype_enum_test(oiio) - aggregate_enum_test(oiio) - vecsemantics_enum_test(oiio) - print ("") - - # Exercise the different constructors, make sure they create the - # correct TypeDesc (also exercises the individual fields, c_str(), - # conversion to string). - breakdown_test (oiio.TypeDesc(), "(default)") - breakdown_test (oiio.TypeDesc(oiio.UINT8), "UINT8") - breakdown_test (oiio.TypeDesc(oiio.HALF, oiio.VEC3, oiio.COLOR), - "HALF, VEC3, COLOR") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.SCALAR, oiio.NOXFORM, 6), - "FLOAT, SCALAR, NOXFORM, array of 6") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.POINT, 2), - "FLOAT, VEC3, POINT, array of 2") - breakdown_test (oiio.TypeDesc(oiio.INT, oiio.VEC2, oiio.BOX, 2), - "INT, VEC2, BOX, array of 2") - breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.BOX, 2), - "FLOAT, VEC3, BOX, array of 2") - print ("") - - # Test construction from a string descriptor - breakdown_test (oiio.TypeDesc("float[2]"), "float[2]") - breakdown_test (oiio.TypeDesc("normal"), "normal") - breakdown_test (oiio.TypeDesc("uint16"), "uint16") - breakdown_test (oiio.TypeDesc("box3"), "box3") - print ("") - - # Test equality, inequality, and equivalent - t_uint8 = oiio.TypeDesc("uint8") - t_uint16 = oiio.TypeDesc("uint16") - t_uint8_b = oiio.TypeDesc("uint8") - print ("uint8 == uint8?", (t_uint8 == t_uint8)) - print ("uint8 == uint8?", (t_uint8 == t_uint8_b)) - print ("uint8 == uint16", (t_uint8 == t_uint16)) - print ("uint8 != uint8?", (t_uint8 != t_uint8)) - print ("uint8 != uint8?", (t_uint8 != t_uint8_b)) - print ("uint8 != uint16", (t_uint8 != t_uint16)) - print ("vector == color", (oiio.TypeDesc("vector") == oiio.TypeDesc("color"))) - print ("vector.equivalent(color)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("color"))) - print ("equivalent(vector,color)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("color"))) - print ("vector.equivalent(float)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("float"))) - print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) - print ("") - - # Exercise property mutation and helper methods that are easy to miss in - # binding ports because they are not just plain constructors/accessors. - t_mut = oiio.TypeDesc() - t_mut.basetype = oiio.FLOAT - t_mut.aggregate = oiio.VEC3 - t_mut.vecsemantics = oiio.COLOR - t_mut.arraylen = 2 - breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") - t_from = oiio.TypeDesc() - t_from.fromstring("point") - breakdown_test (t_from, "fromstring('point')", verbose=False) - t_unarray = oiio.TypeDesc("float[2]") - t_unarray.unarray() - print ("after unarray('float[2]') =", t_unarray) - print ("vector is_vec2,is_vec3,is_vec4 =", - oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), - oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), - oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) - print ("box2i is_box2,is_box3 =", - oiio.TypeDesc("box2i").is_box2(oiio.INT), - oiio.TypeDesc("box2i").is_box3(oiio.INT)) - print ("all_types_equal([uint8,uint8]) =", - oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), - oiio.TypeDesc("uint8")])) - print ("all_types_equal([uint8,uint16]) =", - oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), - oiio.TypeDesc("uint16")])) - print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) - print ("") - - # Exercise implicit conversion paths used by the production pybind11 - # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. - implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) - implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") - print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) - print ("implicit str ImageSpec roi =", implicit_str_spec.roi) - print ("") - - # Test the pre-constructed types - breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) - breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) - breakdown_test (oiio.TypeString, "TypeString", verbose=False) - breakdown_test (oiio.TypeInt, "TypeInt", verbose=False) - breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) - breakdown_test (oiio.TypeInt64, "TypeInt64", verbose=False) - breakdown_test (oiio.TypeUInt64, "TypeUInt64", verbose=False) - breakdown_test (oiio.TypeInt32, "TypeInt32", verbose=False) - breakdown_test (oiio.TypeUInt32, "TypeUInt32", verbose=False) - breakdown_test (oiio.TypeInt16, "TypeInt16", verbose=False) - breakdown_test (oiio.TypeUInt16, "TypeUInt16", verbose=False) - breakdown_test (oiio.TypeInt8, "TypeInt8", verbose=False) - breakdown_test (oiio.TypeUInt8, "TypeUInt8", verbose=False) - breakdown_test (oiio.TypePoint, "TypePoint", verbose=False) - breakdown_test (oiio.TypeVector, "TypeVector", verbose=False) - breakdown_test (oiio.TypeNormal, "TypeNormal", verbose=False) - breakdown_test (oiio.TypeMatrix, "TypeMatrix", verbose=False) - breakdown_test (oiio.TypeMatrix33, "TypeMatrix33", verbose=False) - breakdown_test (oiio.TypeMatrix44, "TypeMatrix44", verbose=False) - breakdown_test (oiio.TypeTimeCode, "TypeTimeCode", verbose=False) - breakdown_test (oiio.TypeKeyCode, "TypeKeyCode", verbose=False) - breakdown_test (oiio.TypeFloat2, "TypeFloat2", verbose=False) - breakdown_test (oiio.TypeVector2, "TypeVector2", verbose=False) - breakdown_test (oiio.TypeFloat4, "TypeFloat4", verbose=False) - breakdown_test (oiio.TypeVector4, "TypeVector4", verbose=False) - breakdown_test (oiio.TypeVector2i, "TypeVector2i", verbose=False) - breakdown_test (oiio.TypeVector3i, "TypeVector3i", verbose=False) - breakdown_test (oiio.TypeHalf, "TypeHalf", verbose=False) - breakdown_test (oiio.TypeRational, "TypeRational", verbose=False) - breakdown_test (oiio.TypeURational, "TypeURational", verbose=False) - breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) - print ("") - - print ("Done.") - except Exception as detail: - print ("Unknown exception:", detail) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) +try: + # Test that all the enum values exist + basetype_enum_test(oiio) + aggregate_enum_test(oiio) + vecsemantics_enum_test(oiio) + print ("") + + # Exercise the different constructors, make sure they create the + # correct TypeDesc (also exercises the individual fields, c_str(), + # conversion to string). + breakdown_test (oiio.TypeDesc(), "(default)") + breakdown_test (oiio.TypeDesc(oiio.UINT8), "UINT8") + breakdown_test (oiio.TypeDesc(oiio.HALF, oiio.VEC3, oiio.COLOR), + "HALF, VEC3, COLOR") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.SCALAR, oiio.NOXFORM, 6), + "FLOAT, SCALAR, NOXFORM, array of 6") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.POINT, 2), + "FLOAT, VEC3, POINT, array of 2") + breakdown_test (oiio.TypeDesc(oiio.INT, oiio.VEC2, oiio.BOX, 2), + "INT, VEC2, BOX, array of 2") + breakdown_test (oiio.TypeDesc(oiio.FLOAT, oiio.VEC3, oiio.BOX, 2), + "FLOAT, VEC3, BOX, array of 2") + print ("") + + # Test construction from a string descriptor + breakdown_test (oiio.TypeDesc("float[2]"), "float[2]") + breakdown_test (oiio.TypeDesc("normal"), "normal") + breakdown_test (oiio.TypeDesc("uint16"), "uint16") + breakdown_test (oiio.TypeDesc("box3"), "box3") + print ("") + + # Test equality, inequality, and equivalent + t_uint8 = oiio.TypeDesc("uint8") + t_uint16 = oiio.TypeDesc("uint16") + t_uint8_b = oiio.TypeDesc("uint8") + print ("uint8 == uint8?", (t_uint8 == t_uint8)) + print ("uint8 == uint8?", (t_uint8 == t_uint8_b)) + print ("uint8 == uint16", (t_uint8 == t_uint16)) + print ("uint8 != uint8?", (t_uint8 != t_uint8)) + print ("uint8 != uint8?", (t_uint8 != t_uint8_b)) + print ("uint8 != uint16", (t_uint8 != t_uint16)) + print ("vector == color", (oiio.TypeDesc("vector") == oiio.TypeDesc("color"))) + print ("vector.equivalent(color)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("color"))) + print ("equivalent(vector,color)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("color"))) + print ("vector.equivalent(float)", oiio.TypeDesc("vector").equivalent(oiio.TypeDesc("float"))) + print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) + print ("") + + # Exercise property mutation and helper methods that are easy to miss in + # binding ports because they are not just plain constructors/accessors. + t_mut = oiio.TypeDesc() + t_mut.basetype = oiio.FLOAT + t_mut.aggregate = oiio.VEC3 + t_mut.vecsemantics = oiio.COLOR + t_mut.arraylen = 2 + breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") + t_from = oiio.TypeDesc() + t_from.fromstring("point") + breakdown_test (t_from, "fromstring('point')", verbose=False) + t_unarray = oiio.TypeDesc("float[2]") + t_unarray.unarray() + print ("after unarray('float[2]') =", t_unarray) + print ("vector is_vec2,is_vec3,is_vec4 =", + oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) + print ("box2i is_box2,is_box3 =", + oiio.TypeDesc("box2i").is_box2(oiio.INT), + oiio.TypeDesc("box2i").is_box3(oiio.INT)) + print ("all_types_equal([uint8,uint8]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint8")])) + print ("all_types_equal([uint8,uint16]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint16")])) + print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) + print ("") + + # Exercise implicit conversion paths used by the production pybind11 + # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. + implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) + implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") + print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) + print ("implicit str ImageSpec roi =", implicit_str_spec.roi) + print ("") + + # Test the pre-constructed types + breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) + breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) + breakdown_test (oiio.TypeString, "TypeString", verbose=False) + breakdown_test (oiio.TypeInt, "TypeInt", verbose=False) + breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) + breakdown_test (oiio.TypeInt64, "TypeInt64", verbose=False) + breakdown_test (oiio.TypeUInt64, "TypeUInt64", verbose=False) + breakdown_test (oiio.TypeInt32, "TypeInt32", verbose=False) + breakdown_test (oiio.TypeUInt32, "TypeUInt32", verbose=False) + breakdown_test (oiio.TypeInt16, "TypeInt16", verbose=False) + breakdown_test (oiio.TypeUInt16, "TypeUInt16", verbose=False) + breakdown_test (oiio.TypeInt8, "TypeInt8", verbose=False) + breakdown_test (oiio.TypeUInt8, "TypeUInt8", verbose=False) + breakdown_test (oiio.TypePoint, "TypePoint", verbose=False) + breakdown_test (oiio.TypeVector, "TypeVector", verbose=False) + breakdown_test (oiio.TypeNormal, "TypeNormal", verbose=False) + breakdown_test (oiio.TypeMatrix, "TypeMatrix", verbose=False) + breakdown_test (oiio.TypeMatrix33, "TypeMatrix33", verbose=False) + breakdown_test (oiio.TypeMatrix44, "TypeMatrix44", verbose=False) + breakdown_test (oiio.TypeTimeCode, "TypeTimeCode", verbose=False) + breakdown_test (oiio.TypeKeyCode, "TypeKeyCode", verbose=False) + breakdown_test (oiio.TypeFloat2, "TypeFloat2", verbose=False) + breakdown_test (oiio.TypeVector2, "TypeVector2", verbose=False) + breakdown_test (oiio.TypeFloat4, "TypeFloat4", verbose=False) + breakdown_test (oiio.TypeVector4, "TypeVector4", verbose=False) + breakdown_test (oiio.TypeVector2i, "TypeVector2i", verbose=False) + breakdown_test (oiio.TypeVector3i, "TypeVector3i", verbose=False) + breakdown_test (oiio.TypeHalf, "TypeHalf", verbose=False) + breakdown_test (oiio.TypeRational, "TypeRational", verbose=False) + breakdown_test (oiio.TypeURational, "TypeURational", verbose=False) + breakdown_test (oiio.TypeUInt, "TypeUInt", verbose=False) + print ("") + + print ("Done.") +except Exception as detail: + print ("Unknown exception:", detail) From 34d839eedc540ecad10faf2e745265befea46557 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 22:49:48 +1100 Subject: [PATCH 24/26] even less less changes Signed-off-by: Aleksandr Motsjonov --- testsuite/python-roi/src/test_roi.py | 3 +++ testsuite/python-typedesc/run.py | 3 +-- .../python-typedesc/src/test_typedesc.py | 23 ++++++++----------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index dbf99152b3..f59ac07548 100755 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -9,6 +9,9 @@ import OpenImageIO as oiio +###################################################################### +# main test starts here + try: r = oiio.ROI() print ("undefined ROI() =", r) diff --git a/testsuite/python-typedesc/run.py b/testsuite/python-typedesc/run.py index 0c94900185..a40c33abb8 100755 --- a/testsuite/python-typedesc/run.py +++ b/testsuite/python-typedesc/run.py @@ -4,5 +4,4 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO - -command += pythonbin + " src/test_typedesc.py " + OIIO_BUILD_ROOT + " > out.txt" +command += pythonbin + " src/test_typedesc.py > out.txt" diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py index 2a3a74c1fe..1c918c85fb 100755 --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -6,18 +6,11 @@ from __future__ import annotations -import pathlib -import sys - -if len(sys.argv) > 1: - build_root = pathlib.Path(sys.argv[1]).resolve() - sys.path.insert(0, str(build_root / "lib/python/site-packages")) - import OpenImageIO as oiio # Test that every expected enum value of BASETYPE exists -def basetype_enum_test(oiio): +def basetype_enum_test(): try: oiio.UNKNOWN oiio.NONE @@ -49,7 +42,7 @@ def basetype_enum_test(oiio): # Test that every expected enum value of AGGREGATE exists -def aggregate_enum_test(oiio): +def aggregate_enum_test(): try: oiio.NOSEMANTICS oiio.SCALAR @@ -64,7 +57,7 @@ def aggregate_enum_test(oiio): # Test that every expected enum value of VECSEMANTICS exists -def vecsemantics_enum_test(oiio): +def vecsemantics_enum_test(): try: oiio.NOXFORM oiio.COLOR @@ -97,11 +90,15 @@ def breakdown_test(t, name="", verbose=True): print (" elementsize =", t.elementsize()) print (" basesize =", t.basesize()) + +###################################################################### +# main test starts here + try: # Test that all the enum values exist - basetype_enum_test(oiio) - aggregate_enum_test(oiio) - vecsemantics_enum_test(oiio) + basetype_enum_test() + aggregate_enum_test() + vecsemantics_enum_test() print ("") # Exercise the different constructors, make sure they create the From 2a7aa6a76e7cf6007c110e0f928dd2cc302b25cc Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 22:58:10 +1100 Subject: [PATCH 25/26] Python version fix Signed-off-by: Aleksandr Motsjonov --- testsuite/runtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testsuite/runtest.py b/testsuite/runtest.py index fcd8b7f001..9184a4569f 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -13,6 +13,7 @@ import filecmp import shutil import shlex +from typing import Optional from optparse import OptionParser @@ -369,7 +370,7 @@ def oiiotool (args: str, silent: bool=False, concat: bool=True, failureok: bool= # the identical name, and if that fails, it will look for alternatives of # the form "basename-*.ext" (or ANY match in the ref directory, if anymatch # is True). -def checkref (name: str, refdirlist: list[str], refname: str|None=None) -> tuple[bool, str]: +def checkref (name: str, refdirlist: list[str], refname: Optional[str]=None) -> tuple[bool, str]: # Break the output into prefix+extension if refname is None: refname = name From ecef018299290a2f373c9b9317f89591c0ed997a Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sun, 15 Mar 2026 23:29:06 +1100 Subject: [PATCH 26/26] fix tests Signed-off-by: Aleksandr Motsjonov --- testsuite/runtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/runtest.py b/testsuite/runtest.py index 9184a4569f..680c3cd905 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -418,7 +418,7 @@ def runtest (command: str, outputs: list[str], failureok: int=0) -> int : err = 0 # print ("working dir = " + tmpdir) os.chdir (srcdir) - for out in outputs: + for out in ("out.txt", "out.err.txt", "out-nanobind.txt"): if os.path.exists(out): os.remove(out) open ("out.txt", "w").close() # truncate out.txt