Skip to content

Commit f556d63

Browse files
committed
feat: Add python stub files for code completion and static analysis
1 parent c5c8fb9 commit f556d63

6 files changed

Lines changed: 184 additions & 18 deletions

File tree

src/cmake/pythonutils.cmake

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,14 @@ macro (setup_python_module)
144144
set (PYTHON_SITE_DIR .)
145145
endif ()
146146

147+
set(PYTHON_BUILD_SITE "${CMAKE_BINARY_DIR}/lib/python/site-packages")
148+
147149
# In the build area, put it in lib/python so it doesn't clash with the
148150
# non-python libraries of the same name (which aren't prefixed by "lib"
149151
# on Windows).
150152
set_target_properties (${target_name} PROPERTIES
151-
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
152-
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
153+
LIBRARY_OUTPUT_DIRECTORY ${PYTHON_BUILD_SITE}
154+
ARCHIVE_OUTPUT_DIRECTORY ${PYTHON_BUILD_SITE}
153155
)
154156

155157
install (TARGETS ${target_name}
@@ -158,5 +160,45 @@ macro (setup_python_module)
158160

159161
install(FILES __init__.py DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
160162

163+
# Create the __init__.pyi stub file
164+
# Define where to create the virtual environment
165+
set(PYTHON_VENV_DIR "${CMAKE_BINARY_DIR}/venv")
166+
167+
if(WIN32)
168+
set(PYTHON_VENV_EXE "${PYTHON_VENV_DIR}/Scripts/python.exe")
169+
else()
170+
set(PYTHON_VENV_EXE "${PYTHON_VENV_DIR}/bin/python")
171+
endif()
172+
173+
# Create the virtualenv if it doesn't exist
174+
add_custom_command(
175+
COMMAND ${Python3_EXECUTABLE} -m venv "${PYTHON_VENV_DIR}"
176+
COMMAND ${PYTHON_VENV_EXE} -m pip install mypy~=1.15.0 stubgenlib~=0.1.0
177+
OUTPUT "${PYTHON_VENV_DIR}/bin/activate"
178+
COMMENT "Creating virtualenv at ${PYTHON_VENV_DIR}"
179+
)
180+
181+
# Run stub generation process
182+
set(_stub_file "${CMAKE_BINARY_DIR}/lib/python/site-packages/OpenImageIO.pyi")
183+
# FIXME: is this the right location to use? the source gets copied to build/src
184+
set(_stub_gen "${CMAKE_SOURCE_DIR}/src/python/generate_stubs.py")
185+
add_custom_command(
186+
COMMAND PYTHONPATH=${PYTHON_BUILD_SITE} ${PYTHON_VENV_EXE} ${_stub_gen} -p OpenImageIO -o ${PYTHON_BUILD_SITE}
187+
OUTPUT ${_stub_file}
188+
DEPENDS "${PYTHON_VENV_DIR}/bin/activate" ${_stub_gen}
189+
COMMENT "Creating python stubs"
190+
)
191+
install(FILES ${_stub_file} DESTINATION ${PYTHON_SITE_DIR} RENAME __init__.pyi COMPONENT user)
192+
# install the marker file
193+
file(WRITE "py.typed" "")
194+
install(FILES "py.typed"DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
195+
196+
# Ensure this runs after PyOpenImageIO
197+
add_custom_target(
198+
PyOpenImageIO_stubs ALL
199+
DEPENDS "${PYTHON_VENV_DIR}/bin/activate" ${_stub_file} ${CMAKE_SOURCE_DIR}/src/python/py.typed
200+
)
201+
add_dependencies(PyOpenImageIO_stubs PyOpenImageIO)
202+
161203
endmacro ()
162204

src/python/generate_stubs.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import absolute_import, annotations, division, print_function
2+
3+
import mypy.stubgen
4+
import mypy.stubgenc
5+
from mypy.stubgenc import SignatureGenerator, DocstringSignatureGenerator
6+
7+
from stubgenlib.siggen import (
8+
AdvancedSignatureGenerator,
9+
AdvancedSigMatcher,
10+
)
11+
from stubgenlib.utils import add_positional_only_args
12+
13+
14+
PY_TO_STDVECTOR_RESULT = "float | list[float] | tuple[float, ...]"
15+
16+
17+
class OIIOSignatureGenerator(AdvancedSignatureGenerator):
18+
sig_matcher = AdvancedSigMatcher(
19+
signature_overrides={
20+
# signatures for these special methods include many inaccurate overloads
21+
"*.__ne__": "(self, other: object) -> bool",
22+
"*.__eq__": "(self, other: object) -> bool",
23+
},
24+
arg_type_overrides={
25+
# FIXME: Buffer may in fact be more accurate here
26+
("*", "*", "Buffer"): "numpy.ndarray",
27+
# these use py_to_stdvector util
28+
("*.ImageBufAlgo.*", "min", "object"): PY_TO_STDVECTOR_RESULT,
29+
("*.ImageBufAlgo.*", "max", "object"): PY_TO_STDVECTOR_RESULT,
30+
("*.ImageBufAlgo.*", "black", "object"): PY_TO_STDVECTOR_RESULT,
31+
("*.ImageBufAlgo.*", "white", "object"): PY_TO_STDVECTOR_RESULT,
32+
("*.ImageBufAlgo.*", "sthresh", "object"): PY_TO_STDVECTOR_RESULT,
33+
("*.ImageBufAlgo.*", "scontrast", "object"): PY_TO_STDVECTOR_RESULT,
34+
("*.ImageBufAlgo.*", "white_balance", "object"): PY_TO_STDVECTOR_RESULT,
35+
("*.ImageBufAlgo.*", "values", "object"): PY_TO_STDVECTOR_RESULT,
36+
("*.ImageBufAlgo.*", "top", "object"): PY_TO_STDVECTOR_RESULT,
37+
("*.ImageBufAlgo.*", "bottom", "object"): PY_TO_STDVECTOR_RESULT,
38+
("*.ImageBufAlgo.*", "topleft", "object"): PY_TO_STDVECTOR_RESULT,
39+
("*.ImageBufAlgo.*", "topright", "object"): PY_TO_STDVECTOR_RESULT,
40+
("*.ImageBufAlgo.*", "bottomleft", "object"): PY_TO_STDVECTOR_RESULT,
41+
("*.ImageBufAlgo.*", "bottomright", "object"): PY_TO_STDVECTOR_RESULT,
42+
("*.ImageBufAlgo.*", "color", "object"): PY_TO_STDVECTOR_RESULT,
43+
# BASETYPE & str are implicitly converible to TypeDesc
44+
("*", "*", "*.TypeDesc"): "Union[TypeDesc, BASETYPE, str]",
45+
# list is not strictly required
46+
("*.ImageOutput.open", "specs", "list[ImageSpec]"): "Iterable[ImageSpec]",
47+
},
48+
result_type_overrides={
49+
# FIXME: is there a way to use std::optional for these?
50+
("*.ImageOutput.create", "object"): "ImageOutput | None",
51+
("*.ImageOutput.open", "object"): "ImageOutput | None",
52+
("*.ImageInput.create", "object"): "ImageInput | None",
53+
("*.ImageInput.open", "object"): "ImageInput | None",
54+
55+
# ("*.TextureSystem.imagespec", "object"): "ImageSpec | None",
56+
# ("*.ImageInput.read_native_deep_*", "object"): "DeepData | None",
57+
58+
# pybind11 has special support, so it may be possible to get it to emit these types
59+
# by using py::numpy in our wrapper code.
60+
("*.ImageInput.read_*", "object"): "numpy.ndarray | None",
61+
("*", "Buffer"): "numpy.ndarray",
62+
("*.get_pixels", "object"): "numpy.ndarray | None",
63+
64+
# For results, object is too restrictive (produces spurious errors during type analysis)
65+
("*.getattribute", "object"): "Any",
66+
("*.ImageSpec.get", "object"): "Any",
67+
68+
# pybind11 does not have a way to emit tuple[T, ...], e.g. from std:vector<T>.
69+
("*.ImageBufAlgo.histogram", "tuple"): "tuple[int, ...]",
70+
("*.ImageBufAlgo.isConstantColor", "*"): "tuple[float, ...] | None",
71+
("*.ImageBufAlgo.color_range_check", "*"): "tuple[int, ...] | None",
72+
("*.TextureSystem.texture", "tuple"): "tuple[float, ...]",
73+
("*.TextureSystem.texture3d", "tuple"): "tuple[float, ...]",
74+
("*.TextureSystem.environment", "tuple"): "tuple[float, ...]",
75+
("*.ImageBuf.getpixel", "tuple"): "tuple[float, ...]",
76+
("*.ImageBuf.interpixel*", "tuple"): "tuple[float, ...]",
77+
("*.ImageSpec.get_channelformats", "tuple"): "tuple[TypeDesc, ...]",
78+
},
79+
80+
property_type_overrides={
81+
# FIXME: this isn't working
82+
("*.ParamValue.value", "object"): "Any",
83+
},
84+
)
85+
86+
def process_sig(
87+
self, ctx: mypy.stubgen.FunctionContext, sig: mypy.stubgen.FunctionSig
88+
) -> mypy.stubgen.FunctionSig:
89+
# Analyze the signature and add a '/' argument if necessary to mark
90+
# arguments which cannot be access by name.
91+
return add_positional_only_args(ctx, super().process_sig(ctx, sig))
92+
93+
94+
class InspectionStubGenerator(mypy.stubgenc.InspectionStubGenerator):
95+
def get_sig_generators(self) -> list[SignatureGenerator]:
96+
return [
97+
OIIOSignatureGenerator(
98+
fallback_sig_gen=DocstringSignatureGenerator(),
99+
)
100+
]
101+
102+
def set_defined_names(self, defined_names: set[str]) -> None:
103+
super().set_defined_names(defined_names)
104+
for typ in ["Any", "Iterable"]:
105+
self.add_name(f"typing.{typ}", require=False)
106+
107+
108+
mypy.stubgen.InspectionStubGenerator = InspectionStubGenerator # type: ignore[attr-defined,misc]
109+
mypy.stubgenc.InspectionStubGenerator = InspectionStubGenerator # type: ignore[misc]
110+
111+
if __name__ == "__main__":
112+
mypy.stubgen.main()

src/python/py.typed

Whitespace-only changes.

src/python/py_imagebufalgo.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "py_oiio.h"
66
#include <OpenImageIO/color.h>
77
#include <OpenImageIO/imagebufalgo.h>
8+
#include <optional>
89

910

1011
namespace PyOpenImageIO {
@@ -2172,7 +2173,7 @@ IBA_ocionamedtransform_colorconfig_ret(
21722173

21732174

21742175

2175-
py::object
2176+
std::optional<py::tuple>
21762177
IBA_isConstantColor(const ImageBuf& src, float threshold, ROI roi = ROI::All(),
21772178
int nthreads = 0)
21782179
{
@@ -2186,7 +2187,7 @@ IBA_isConstantColor(const ImageBuf& src, float threshold, ROI roi = ROI::All(),
21862187
if (r) {
21872188
return C_to_tuple(&constcolor[0], (int)constcolor.size());
21882189
} else {
2189-
return py::none();
2190+
return std::nullopt;
21902191
}
21912192
}
21922193

@@ -2221,7 +2222,7 @@ IBA_nonzero_region(const ImageBuf& src, ROI roi, int nthreads)
22212222

22222223

22232224

2224-
py::object
2225+
std::optional<py::tuple>
22252226
IBA_color_range_check(ImageBuf& src, const py::object& low,
22262227
const py::object& high, ROI roi, int nthreads)
22272228
{
@@ -2242,11 +2243,10 @@ IBA_color_range_check(ImageBuf& src, const py::object& low,
22422243
counts[0] = lowcount;
22432244
counts[1] = highcount;
22442245
counts[2] = inrangecount;
2245-
result = C_to_tuple<int64_t>(counts);
2246+
return C_to_tuple<int64_t>(counts);
22462247
} else {
2247-
result = py::none();
2248+
return std::nullopt;
22482249
}
2249-
return result;
22502250
}
22512251

22522252

@@ -2351,7 +2351,7 @@ IBA_text_size(const std::string& text, int fontsize = 16,
23512351

23522352

23532353

2354-
py::object
2354+
py::tuple
23552355
IBA_histogram(const ImageBuf& src, int channel = 0, int bins = 256,
23562356
float min = 0.0f, float max = 1.0f, bool ignore_empty = false,
23572357
ROI roi = {}, int nthreads = 0)

src/python/py_imageinput.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

55
#include "py_oiio.h"
6+
#include <optional>
67

78
namespace PyOpenImageIO {
89

@@ -114,7 +115,7 @@ ImageInput_read_tiles(ImageInput& self, int subimage, int miplevel, int xbegin,
114115

115116

116117

117-
py::object
118+
std::optional<DeepData>
118119
ImageInput_read_native_deep_scanlines(ImageInput& self, int subimage,
119120
int miplevel, int ybegin, int yend, int z,
120121
int chbegin, int chend)
@@ -127,12 +128,15 @@ ImageInput_read_native_deep_scanlines(ImageInput& self, int subimage,
127128
ok = self.read_native_deep_scanlines(subimage, miplevel, ybegin, yend,
128129
z, chbegin, chend, *dd);
129130
}
130-
return ok ? py::cast(dd.release()) : py::none();
131+
if (!ok) {
132+
return std::nullopt;
133+
}
134+
return *dd.release();
131135
}
132136

133137

134138

135-
py::object
139+
std::optional<DeepData>
136140
ImageInput_read_native_deep_tiles(ImageInput& self, int subimage, int miplevel,
137141
int xbegin, int xend, int ybegin, int yend,
138142
int zbegin, int zend, int chbegin, int chend)
@@ -146,12 +150,15 @@ ImageInput_read_native_deep_tiles(ImageInput& self, int subimage, int miplevel,
146150
ybegin, yend, zbegin, zend, chbegin,
147151
chend, *dd);
148152
}
149-
return ok ? py::cast(dd.release()) : py::none();
153+
if (!ok) {
154+
return std::nullopt;
155+
}
156+
return *dd.release();
150157
}
151158

152159

153160

154-
py::object
161+
std::optional<DeepData>
155162
ImageInput_read_native_deep_image(ImageInput& self, int subimage, int miplevel)
156163
{
157164
std::unique_ptr<DeepData> dd;
@@ -161,7 +168,10 @@ ImageInput_read_native_deep_image(ImageInput& self, int subimage, int miplevel)
161168
dd.reset(new DeepData);
162169
ok = self.read_native_deep_image(subimage, miplevel, *dd);
163170
}
164-
return ok ? py::cast(dd.release()) : py::none();
171+
if (!ok) {
172+
return std::nullopt;
173+
}
174+
return *dd.release();
165175
}
166176

167177

src/python/py_texturesys.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// https://github.com/AcademySoftwareFoundation/OpenImageIO
44

55
#include "py_oiio.h"
6+
#include <optional>
67

78
namespace PyOpenImageIO {
89

@@ -295,14 +296,14 @@ declare_texturesystem(py::module& m)
295296
.def(
296297
"imagespec",
297298
[](TextureSystemWrap& ts, const std::string& filename,
298-
int subimage) -> py::object {
299+
int subimage) -> std::optional<ImageSpec> {
299300
py::gil_scoped_release gil;
300301
const ImageSpec* spec
301302
= ts.m_texsys->imagespec(ustring(filename), subimage);
302303
if (!spec) {
303-
return py::none();
304+
return std::nullopt;
304305
}
305-
return py::object(py::cast(*spec));
306+
return *spec;
306307
},
307308
"filename"_a, "subimage"_a = 0)
308309
.def(
@@ -321,6 +322,7 @@ declare_texturesystem(py::module& m)
321322
},
322323
"filename"_a, "s"_a, "t"_a)
323324
.def(
325+
// FIXME: use std:tuple here
324326
"inventory_udim",
325327
[](TextureSystemWrap& ts, const std::string& filename) {
326328
// Return a tuple containing (nutiles, nvtiles, filenames)

0 commit comments

Comments
 (0)