Skip to content

Commit 963d2f5

Browse files
authored
Merge pull request #419 from Simple-Robotics/update-nanobind
Use nanobind's new recursive stub generation, fix dynamic module handling
2 parents 7f0a1bc + 178d098 commit 963d2f5

File tree

3 files changed

+67
-31
lines changed

3 files changed

+67
-31
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
- Recursive stub generation for Python bindings ([#419](https://github.com/Simple-Robotics/proxsuite/pull/419))
11+
912
### Changed
1013
- Change the default branch to `devel` ([#395](https://github.com/Simple-Robotics/proxsuite/pull/395))
1114
- Change `dual_feasibility` test threshold in `sparse_maros_meszaros` unit test ([#403](https://github.com/Simple-Robotics/proxsuite/pull/403))
1215
- replace `std::numeric_limits<T>::infinity()` by `std::numeric_limits<T>::max()` ([#413](https://github.com/Simple-Robotics/proxsuite/pull/413))
1316
- Upgraded nanobind dependency version (submodule) to v2.9.2 ([#418](https://github.com/Simple-Robotics/proxsuite/pull/418))
17+
- Better dynamic module handling ([#419](https://github.com/Simple-Robotics/proxsuite/pull/419))
1418

1519
### Fixed
1620
- Use the right table to store `configure-args` cmeel argument ([#403](https://github.com/Simple-Robotics/proxsuite/pull/403))

bindings/python/CMakeLists.txt

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ if(NOT nanobind_FOUND)
3939
external/nanobind
4040
${CMAKE_CURRENT_BINARY_DIR}/external/nanobind
4141
)
42+
message(STATUS "Will use nanobind submodule.")
4243
else()
4344
message(STATUS "Found installed nanobind.")
4445
endif()
@@ -107,12 +108,18 @@ function(list_filter list regular_expression dest_list)
107108
set(${dest_list} ${list} PARENT_SCOPE)
108109
endfunction(list_filter)
109110

110-
function(CREATE_PYTHON_TARGET target_name COMPILE_OPTIONS dependencies)
111+
function(
112+
create_python_target
113+
target_name
114+
compile_options
115+
dependencies
116+
generate_stubs
117+
)
111118
nanobind_add_module(${target_name} ${PYWRAP_SOURCES} ${PYWRAP_HEADERS})
112119
add_dependencies(${PROJECT_NAME}_python ${target_name})
113120

114121
target_link_libraries(${target_name} PUBLIC ${dependencies})
115-
target_compile_options(${target_name} PRIVATE ${COMPILE_OPTIONS})
122+
target_compile_options(${target_name} PRIVATE ${compile_options})
116123
target_link_libraries(${target_name} PRIVATE proxsuite)
117124
target_compile_definitions(
118125
${target_name}
@@ -133,7 +140,7 @@ function(CREATE_PYTHON_TARGET target_name COMPILE_OPTIONS dependencies)
133140
if(LINK_PYTHON_INTERFACE_TO_OPENMP)
134141
target_link_libraries(${target_name} PRIVATE ${OpenMP_CXX_LIBRARIES})
135142
endif(LINK_PYTHON_INTERFACE_TO_OPENMP)
136-
else(BUILD_WITH_OPENMP_SUPPORT)
143+
else()
137144
list_filter("${PYWRAP_HEADERS}" "expose-parallel" PYWRAP_HEADERS)
138145
endif(BUILD_WITH_OPENMP_SUPPORT)
139146

@@ -173,16 +180,26 @@ function(CREATE_PYTHON_TARGET target_name COMPILE_OPTIONS dependencies)
173180
endif()
174181

175182
install(TARGETS ${target_name} DESTINATION ${${PYWRAP}_INSTALL_DIR})
176-
if(GENERATE_PYTHON_STUBS)
183+
if(${generate_stubs})
184+
set(
185+
stub_outputs
186+
${target_name}/__init__.pyi
187+
${target_name}/helpers.pyi
188+
${target_name}/proxqp/__init__.pyi
189+
${target_name}/proxqp/dense.pyi
190+
${target_name}/proxqp/sparse.pyi
191+
)
177192
nanobind_add_stub(
178193
${target_name}_stub
179194
MODULE ${target_name}
180-
OUTPUT ${target_name}.pyi
195+
OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}
196+
OUTPUT ${stub_outputs}
197+
RECURSIVE
181198
PYTHON_PATH $<TARGET_FILE_DIR:${target_name}>
182199
DEPENDS ${target_name}
183200
)
184201
install(
185-
FILES ${CMAKE_CURRENT_BINARY_DIR}/${target_name}.pyi
202+
DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${target_name}
186203
DESTINATION ${${PYWRAP}_INSTALL_DIR}
187204
)
188205
endif()
@@ -200,23 +217,25 @@ else()
200217
set(AVX512_COMPILE_OPTION "-mavx512f")
201218
endif()
202219

203-
CREATE_PYTHON_TARGET(proxsuite_pywrap "" proxsuite)
220+
create_python_target(proxsuite_pywrap "" proxsuite ${GENERATE_PYTHON_STUBS})
204221
if(
205222
BUILD_WITH_VECTORIZATION_SUPPORT
206223
AND ${CMAKE_SYSTEM_PROCESSOR} MATCHES "(x86)|(X86)|(amd64)|(AMD64)"
207224
)
208225
if(BUILD_BINDINGS_WITH_AVX2_SUPPORT)
209-
CREATE_PYTHON_TARGET(
226+
create_python_target(
210227
proxsuite_pywrap_avx2
211228
"${AVX2_COMPILE_OPTION};${FMA_COMPILE_OPTION}"
212229
proxsuite-vectorized
230+
FALSE
213231
)
214232
endif(BUILD_BINDINGS_WITH_AVX2_SUPPORT)
215233
if(BUILD_BINDINGS_WITH_AVX512_SUPPORT)
216-
CREATE_PYTHON_TARGET(
234+
create_python_target(
217235
proxsuite_pywrap_avx512
218236
"${AVX512_COMPILE_OPTION};${FMA_COMPILE_OPTION}"
219237
proxsuite-vectorized
238+
FALSE
220239
)
221240
endif(BUILD_BINDINGS_WITH_AVX512_SUPPORT)
222241
endif()
Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,54 @@
1-
import platform
1+
"""
2+
Copyright (c) 2022-2025, INRIA
3+
"""
4+
25
import numpy # noqa F401 for OpenMP proper linkage
6+
from typing import TYPE_CHECKING
37

4-
machine = platform.machine()
5-
has_vectorization_instructions = not machine.startswith(
6-
("arm", "aarch64", "power", "ppc64", "s390x", "sparc")
7-
)
8-
if has_vectorization_instructions:
9-
from . import instructionset
8+
if TYPE_CHECKING:
9+
from .proxsuite_pywrap import * # noqa F403
1010

1111

12-
def load_main_module(globals):
13-
def load_module(main_module_name):
14-
import importlib
12+
def _load_main_module():
13+
import platform
14+
import importlib
15+
16+
machine = platform.machine()
17+
has_vectorization_instructions = not machine.startswith(
18+
("arm", "aarch64", "power", "ppc64", "s390x", "sparc")
19+
)
1520

21+
def load_module(main_module_name):
1622
try:
17-
main_module = importlib.import_module("." + main_module_name, __name__)
18-
globals.update(main_module.__dict__)
19-
del globals[main_module_name]
20-
return True
23+
return importlib.import_module("." + main_module_name, __name__)
2124
except ModuleNotFoundError:
2225
return False
2326

2427
if has_vectorization_instructions: # noqa
28+
from . import instructionset
29+
2530
all_modules = [
2631
("proxsuite_pywrap_avx512", instructionset.has_AVX512F),
2732
("proxsuite_pywrap_avx2", instructionset.has_AVX2),
2833
]
2934

3035
for module_name, checker in all_modules:
31-
if checker() and load_module(module_name):
32-
return
36+
if checker() and (mod := load_module(module_name)):
37+
return mod
38+
39+
return load_module("proxsuite_pywrap")
40+
41+
42+
_submodule = _load_main_module()
43+
3344

34-
assert load_module("proxsuite_pywrap")
45+
def __getattr__(name: str):
46+
# reroute proxsuite module's attributes to be that of the loaded submodule.
47+
return getattr(_submodule, name)
3548

3649

37-
load_main_module(globals=globals())
38-
del load_main_module
39-
del platform
40-
del has_vectorization_instructions
41-
del machine
50+
def __dir__():
51+
# returns iterable of all accessible attributes in the module.
52+
# implement this for instropection, for e.g. autocomplete in interpreters (IPython).
53+
# We return the attributes from the submodule.
54+
return dir(_submodule)

0 commit comments

Comments
 (0)