Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate Python stubs #1818

Merged
merged 26 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/generic-ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ runs:
run: |
python -m pip install --upgrade pip
python -m pip install pytest==8.0.0
python -m pip install pybind11_stubgen

- name: Setup Directories
shell: bash
Expand Down Expand Up @@ -151,6 +152,7 @@ runs:
-DCMAKE_PREFIX_PATH:PATH=$(pwd)/../dependencies/install/
-DF3D_BINDINGS_JAVA=${{ (runner.os != 'macOS' || inputs.cpu == 'arm64') && inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
-DF3D_BINDINGS_PYTHON=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
-DF3D_BINDINGS_PYTHON_GENERATE_STUBS=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
-DF3D_EXCLUDE_DEPRECATED=${{ inputs.exclude_deprecated_label == 'exclude-deprecated' && 'ON' || 'OFF' }}
-DF3D_LINUX_GENERATE_MAN=ON
-DF3D_LINUX_INSTALL_DEFAULT_CONFIGURATION_FILE_IN_PREFIX=ON
Expand Down Expand Up @@ -198,6 +200,12 @@ runs:
with:
path: ${{github.workspace}}\build\bin_Release

- name: Install Mesa Windows Python
if: runner.os == 'Windows'
uses: f3d-app/install-mesa-windows-action@v1
with:
path: ${{github.workspace}}\build\Release\f3d

# A EGL test is crashing in the CI but not locally
- name: Set CI test exception for Linux EGL
if:
Expand Down
1 change: 1 addition & 0 deletions doc/dev/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Some modules, plugins and language bindings depending on external libraries can
* `F3D_PLUGIN_BUILD_USD`: Support for USD file format. Requires `OpenUSD`. Disabled by default.
* `F3D_PLUGIN_BUILD_VDB`: Support for VDB file format. Requires that VTK has been built with `IOOpenVDB` module (and `OpenVDB`). Disabled by default.
* `F3D_BINDINGS_PYTHON`: Generate python bindings (requires `Python` and `pybind11`). Disabled by default.
* `F3D_BINDINGS_PYTHON_GENERATE_STUBS`: Generate python stubs (requires `Python` and `pybind11_stubgen`). Disabled by default.
* `F3D_BINDINGS_JAVA`: Generate java bindings (requires `Java` and `JNI`). Disabled by default.

Some dependencies are provided internally, eg: ImGui, dmon and others. Use `F3D_USE_EXTERNAL_*` to use an external version of these libraries.
Expand Down
5 changes: 5 additions & 0 deletions doc/libf3d/LANGUAGE_BINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ eng.interactor.start()

You can see more examples using python bindings in the dedicated example folder [here](https://github.com/f3d-app/f3d/tree/master/examples/libf3d/python).

### Stubs

It's also possible to generate Python stubs automatically by enabling the CMake option `F3D_BINDINGS_PYTHON_GENERATE_STUBS`.
Python stubs are `.pyi` files defining the public interface, allowing IDEs to auto-complete and do static analysis.

## Java (experimental)

If the Java bindings have been generated using the `F3D_BINDINGS_JAVA` CMake option, the libf3d can be used directly from Java.
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["scikit-build-core", "setuptools-scm>=8.0"]
requires = ["scikit-build-core", "setuptools-scm>=8.0", "pybind11_stubgen"]
build-backend = "scikit_build_core.build"

[project]
name = "f3d"
requires-python = ">=3.8"
requires-python = ">=3.9"
dynamic = ["version"]
description = "F3D, a fast and minimalist 3D viewer"
readme = "README.md"
Expand Down Expand Up @@ -58,6 +58,7 @@ fallback_version = "2.5.1"
CMAKE_OSX_DEPLOYMENT_TARGET = "10.15"
BUILD_SHARED_LIBS = "ON"
F3D_BINDINGS_PYTHON = "ON"
F3D_BINDINGS_PYTHON_GENERATE_STUBS = "ON"
F3D_PLUGINS_STATIC_BUILD = "ON"
F3D_BUILD_APPLICATION = "OFF"
F3D_WINDOWS_BUILD_SHELL_THUMBNAILS_EXTENSION = "OFF"
Expand Down
26 changes: 23 additions & 3 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ cmake_minimum_required(VERSION 3.21)

project(pyf3d)

option(F3D_BINDINGS_PYTHON_GENERATE_STUBS "Generate Python stubs" OFF)

list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/cmake")
include(GNUInstallDirs)
include(f3dPython)

find_package(Python 3.8 COMPONENTS Interpreter Development)
find_package(Python 3.9 COMPONENTS Interpreter Development)
find_package(pybind11 2.2 REQUIRED)

pybind11_add_module(pyf3d MODULE F3DPythonBindings.cxx)
Expand All @@ -25,7 +27,8 @@ set(f3d_python_package_name "f3d")
set(f3d_python_package_suffix "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages")

get_property(F3D_MULTI_CONFIG_GENERATOR GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
set(f3d_module_dir "${CMAKE_BINARY_DIR}$<${F3D_MULTI_CONFIG_GENERATOR}:/$<CONFIG>>/${f3d_python_package_name}")
set(f3d_binary_dir "${CMAKE_BINARY_DIR}$<${F3D_MULTI_CONFIG_GENERATOR}:/$<CONFIG>>")
set(f3d_module_dir "${f3d_binary_dir}/${f3d_python_package_name}")

set_target_properties(pyf3d PROPERTIES
CXX_STANDARD 17
Expand Down Expand Up @@ -56,7 +59,11 @@ if(WIN32)
if(PROJECT_IS_TOP_LEVEL)
f3d_python_windows_dll_fixup(PATHS "$<TARGET_FILE_DIR:f3d::libf3d>" OUTPUT F3D_ABSOLUTE_DLLS_FIXUP)
else()
f3d_python_windows_dll_fixup(PATHS "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" "$<TARGET_FILE_DIR:VTK::CommonCore>" OUTPUT F3D_ABSOLUTE_DLLS_FIXUP)
set(f3d_win_dll_paths "$<TARGET_FILE_DIR:VTK::CommonCore>")
if(BUILD_SHARED_LIBS)
list(APPEND f3d_win_dll_paths "$<TARGET_FILE_DIR:libf3d>")
endif()
f3d_python_windows_dll_fixup(PATHS "${f3d_win_dll_paths}" OUTPUT F3D_ABSOLUTE_DLLS_FIXUP)
endif()
endif()

Expand All @@ -83,6 +90,15 @@ endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/__init__.py.in"
"${CMAKE_CURRENT_BINARY_DIR}/__init__.py-install" @ONLY)

# Stubs
if (F3D_BINDINGS_PYTHON_GENERATE_STUBS)
add_custom_command(
TARGET pyf3d
POST_BUILD
COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/generate_stubs.py --into=${f3d_binary_dir}
Meakk marked this conversation as resolved.
Show resolved Hide resolved
WORKING_DIRECTORY ${f3d_binary_dir})
endif()

# testing
if(BUILD_TESTING)
add_subdirectory(testing)
Expand All @@ -93,3 +109,7 @@ install(TARGETS pyf3d
LIBRARY DESTINATION ${f3d_python_install_path} COMPONENT python)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/__init__.py-install" RENAME "__init__.py"
DESTINATION ${f3d_python_install_path} COMPONENT python)
if (F3D_BINDINGS_PYTHON_GENERATE_STUBS)
install(FILES "${f3d_module_dir}/__init__.pyi" "${f3d_module_dir}/pyf3d.pyi"
DESTINATION ${f3d_python_install_path} COMPONENT python)
endif()
79 changes: 40 additions & 39 deletions python/F3DPythonBindings.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ PYBIND11_MODULE(pyf3d, module)
.def("load_animation_time", &f3d::scene::loadAnimationTime)
.def("animation_time_range", &f3d::scene::animationTimeRange);

// f3d::camera_state_t
py::class_<f3d::camera_state_t>(module, "CameraState")
.def(py::init<>())
.def(py::init<const f3d::point3_t&, const f3d::point3_t&, const f3d::vector3_t&,
const f3d::angle_deg_t&>())
.def_readwrite("position", &f3d::camera_state_t::position)
.def_readwrite("focal_point", &f3d::camera_state_t::focalPoint)
.def_readwrite("view_up", &f3d::camera_state_t::viewUp)
.def_readwrite("view_angle", &f3d::camera_state_t::viewAngle);

// f3d::camera
py::class_<f3d::camera, std::unique_ptr<f3d::camera, py::nodelete>> camera(module, "Camera");
camera //
Expand All @@ -320,15 +330,6 @@ PYBIND11_MODULE(pyf3d, module)
.def("reset_to_default", &f3d::camera::resetToDefault)
.def("reset_to_bounds", &f3d::camera::resetToBounds, py::arg("zoom_factor") = 0.9);

py::class_<f3d::camera_state_t>(module, "CameraState")
.def(py::init<>())
.def(py::init<const f3d::point3_t&, const f3d::point3_t&, const f3d::vector3_t&,
const f3d::angle_deg_t&>())
.def_readwrite("position", &f3d::camera_state_t::position)
.def_readwrite("focal_point", &f3d::camera_state_t::focalPoint)
.def_readwrite("view_up", &f3d::camera_state_t::viewUp)
.def_readwrite("view_angle", &f3d::camera_state_t::viewAngle);

// f3d::window
py::class_<f3d::window, std::unique_ptr<f3d::window, py::nodelete>> window(module, "Window");

Expand Down Expand Up @@ -367,6 +368,28 @@ PYBIND11_MODULE(pyf3d, module)
.def("get_display_from_world", &f3d::window::getDisplayFromWorld,
"Get display coordinate point from world coordinate");

// libInformation
py::class_<f3d::engine::libInformation>(module, "LibInformation")
.def_readonly("version", &f3d::engine::libInformation::Version)
.def_readonly("version_full", &f3d::engine::libInformation::VersionFull)
.def_readonly("build_date", &f3d::engine::libInformation::BuildDate)
.def_readonly("build_system", &f3d::engine::libInformation::BuildSystem)
.def_readonly("compiler", &f3d::engine::libInformation::Compiler)
.def_readonly("modules", &f3d::engine::libInformation::Modules)
.def_readonly("vtk_version", &f3d::engine::libInformation::VTKVersion)
.def_readonly("copyrights", &f3d::engine::libInformation::Copyrights)
.def_readonly("license", &f3d::engine::libInformation::License);

// readerInformation
py::class_<f3d::engine::readerInformation>(module, "ReaderInformation")
.def_readonly("name", &f3d::engine::readerInformation::Name)
.def_readonly("description", &f3d::engine::readerInformation::Description)
.def_readonly("extensions", &f3d::engine::readerInformation::Extensions)
.def_readonly("mime_types", &f3d::engine::readerInformation::MimeTypes)
.def_readonly("plugin_name", &f3d::engine::readerInformation::PluginName)
.def_readonly("has_scene_reader", &f3d::engine::readerInformation::HasSceneReader)
.def_readonly("has_geometry_reader", &f3d::engine::readerInformation::HasGeometryReader);

// f3d::engine
py::class_<f3d::engine> engine(module, "Engine");

Expand Down Expand Up @@ -407,44 +430,22 @@ PYBIND11_MODULE(pyf3d, module)
.def_static("get_readers_info", &f3d::engine::getReadersInfo)
.def_static("get_rendering_backend_list", &f3d::engine::getRenderingBackendList);

// libInformation
py::class_<f3d::engine::libInformation>(module, "LibInformation")
.def_readonly("version", &f3d::engine::libInformation::Version)
.def_readonly("version_full", &f3d::engine::libInformation::VersionFull)
.def_readonly("build_date", &f3d::engine::libInformation::BuildDate)
.def_readonly("build_system", &f3d::engine::libInformation::BuildSystem)
.def_readonly("compiler", &f3d::engine::libInformation::Compiler)
.def_readonly("modules", &f3d::engine::libInformation::Modules)
.def_readonly("vtk_version", &f3d::engine::libInformation::VTKVersion)
.def_readonly("copyrights", &f3d::engine::libInformation::Copyrights)
.def_readonly("license", &f3d::engine::libInformation::License);

// readerInformation
py::class_<f3d::engine::readerInformation>(module, "ReaderInformation")
.def_readonly("name", &f3d::engine::readerInformation::Name)
.def_readonly("description", &f3d::engine::readerInformation::Description)
.def_readonly("extensions", &f3d::engine::readerInformation::Extensions)
.def_readonly("mime_types", &f3d::engine::readerInformation::MimeTypes)
.def_readonly("plugin_name", &f3d::engine::readerInformation::PluginName)
.def_readonly("has_scene_reader", &f3d::engine::readerInformation::HasSceneReader)
.def_readonly("has_geometry_reader", &f3d::engine::readerInformation::HasGeometryReader);

// f3d::log
py::class_<f3d::log> log(module, "Log");

log //
.def_static("set_verbose_level", &f3d::log::setVerboseLevel, py::arg("level"),
py::arg("force_std_err") = false)
.def_static("set_use_coloring", &f3d::log::setUseColoring)
.def_static("print",
[](f3d::log::VerboseLevel& level, const std::string& message)
{ f3d::log::print(level, message); });

py::enum_<f3d::log::VerboseLevel>(log, "VerboseLevel")
.value("DEBUG", f3d::log::VerboseLevel::DEBUG)
.value("INFO", f3d::log::VerboseLevel::INFO)
.value("WARN", f3d::log::VerboseLevel::WARN)
.value("ERROR", f3d::log::VerboseLevel::ERROR)
.value("QUIET", f3d::log::VerboseLevel::QUIET)
.export_values();

log //
Meakk marked this conversation as resolved.
Show resolved Hide resolved
.def_static("set_verbose_level", &f3d::log::setVerboseLevel, py::arg("level"),
py::arg("force_std_err") = false)
.def_static("set_use_coloring", &f3d::log::setUseColoring)
.def_static("print",
[](f3d::log::VerboseLevel& level, const std::string& message)
{ f3d::log::print(level, message); });
}
20 changes: 12 additions & 8 deletions python/__init__.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
# Refer to python/__init__.py.in source file

import os
import sys
import re
import sys
import warnings
from pathlib import Path
from typing import Any, Iterable, Mapping, Union

F3D_ABSOLUTE_DLLS = [
# @F3D_ABSOLUTE_DLLS_FIXUP@
Expand All @@ -32,7 +33,9 @@ __version__ = "@F3D_VERSION@"
# monkey patch `options.update`


def f3d_options_update(self, arg):
def _f3d_options_update(
self, arg: Union[Mapping[str, Any], Iterable[tuple[str, Any]]]
) -> None:
try:
for k, v in arg.items():
self[k] = v
Expand All @@ -47,40 +50,41 @@ def f3d_options_update(self, arg):
except AttributeError:
pass

raise ValueError(f"cannot update {self} from {args}")
raise ValueError(f"cannot update {self} from {arg}")


Options.update = f3d_options_update
Options.update = _f3d_options_update


################################################################################
# add deprecated warnings


def deprecated_decorator(f, reason):
def _deprecated_decorator(f, reason):
def g(*args, **kwargs):
warnings.warn(reason, DeprecationWarning, 2)
return f(*args, **kwargs)

return g


def add_deprecation_warnings():
def _add_deprecation_warnings():
for f3d_class in (
Camera,
Scene,
Options,
Interactor,
Engine,
Window,
Image,
):
for name, member in f3d_class.__dict__.items():
if callable(member) and member.__doc__:
m = re.search(r"DEPRECATED(:\s*.+)?", member.__doc__)
if m:
reason = m.group(1) or ""
msg = f"{f3d_class.__qualname__}.{name} is deprecated{reason}"
setattr(f3d_class, name, deprecated_decorator(member, msg))
setattr(f3d_class, name, _deprecated_decorator(member, msg))


add_deprecation_warnings()
_add_deprecation_warnings()
Loading
Loading