Skip to content

Conversation

LukasLendvorsky
Copy link

@LukasLendvorsky LukasLendvorsky commented Jun 27, 2025

Nanobind stub generators supports --recursive option to generate stubs also for submodules. However cmake function nanobind_add_stub doesn't support passing this option. This adds option to also specify RECURSIVE from cmake.

@Ahajha
Copy link
Contributor

Ahajha commented Jun 27, 2025

FYI there's another PR to do this here: #463

@@ -659,6 +667,7 @@ function (nanobind_add_stub name)
if (NOT ARG_INSTALL_TIME)
add_custom_command(
OUTPUT ${NB_STUBGEN_OUTPUTS}
COMMAND ${CMAKE_COMMAND} -E rm -rf ${ARG_OUTPUT}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This directory may already have things in it which the user wants to keep. For example, installing directly into a wheel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When directory already exists, stubgen will refuse to do anything and report error. So this will not be supported regardless even without deleting the folder.

@LukasLendvorsky
Copy link
Author

LukasLendvorsky commented Jun 27, 2025

FYI there's another PR to do this here: #463

I see, thank you. In that case feel free to close this, but please finish that other PR.

@Ahajha
Copy link
Contributor

Ahajha commented Jun 27, 2025

That's not my PR :) Just commenting to show there's some prior art, it's had a lot of design back-and-forth. Not necessarily saying this should be closed.

Non-empty output might cause issues when called with recursive option
@LukasLendvorsky LukasLendvorsky force-pushed the add_recursive_support_to_cmake branch from 41c7710 to 0026300 Compare August 4, 2025 08:08
@keltonhalbert
Copy link

Whether it is this, or #463, this is an important feature that is lacking in the CMake stubgen, and would advocate for merging one. #463 hasn't had any activity in a year.

@wjakob
Copy link
Owner

wjakob commented Aug 26, 2025

Sorry about the huge delay. I am in principle in favor of this change. I have not merged it because accepting this PR requires me to first investigate whether this recursive generation approach works for my own projects like Dr.Jit, and I have not had the time to do that.

Can you clarify what is the difference between these two PRs? Which one is better? ;) @laggykiller @Ahajha

@keltonhalbert
Copy link

I can't speak to the other questions, but I checked out this PR into a personal fork to test it in my project, and it seemed to work like a charm.

nanobind_add_module(calc LTO calc.cpp)
nanobind_add_stub(
    calc_stub
    MODULE calc
    RECURSIVE
    OUTPUT "calc" 
    PYTHON_PATH "$<TARGET_FILE_DIR:calc>"
    MARKER_FILE "py.typed"
    DEPENDS calc
...
target_link_libraries(calc PRIVATE SHARPlib)
install(TARGETS calc DESTINATION nwsspc/sharp)
install(
    FILES
        "${CMAKE_CURRENT_BINARY_DIR}/py.typed"
    DESTINATION "nwsspc/sharp"
)
install(
    DIRECTORY
        "${CMAKE_CURRENT_BINARY_DIR}/calc/"
    DESTINATION "nwsspc/sharp"
)

Tested with PyRight and function signatures and docs show up in my LSP.

@laggykiller
Copy link
Contributor

laggykiller commented Aug 28, 2025

It's been a year since I wrote my PR, I can't remember exactly why I thought writing *_stub.tmp are necessary in my PR. I did remember this gave me the most headache:

#463 (comment)

In a submodule my_ext.sub, how will you know whether to generate my_ext/sub/__init__.pyi or my_ext/sub.pyi? The difference is important especially in larger binding projects containing a mixture of Python and C++ code.

Does this PR ensure stubgen is run only when necessary?

I think this PR looks cleaner than my solution, only small concern from me is COMMAND ${CMAKE_COMMAND} -E rm -rf ${ARG_OUTPUT} accidentally nuking user's files. Does COMMAND ${CMAKE_COMMAND} -E rm -rf ${ARG_OUTPUT}/**.pyi works instead?

@LukasLendvorsky
Copy link
Author

Does COMMAND ${CMAKE_COMMAND} -E rm -rf ${ARG_OUTPUT}/**.pyi works instead?

If I remember correctly, stubgen will return error if the folder already exists. So user files are not really an option. If you want to have some other files in there, you need to put them in after generating stubs.

@Ahajha
Copy link
Contributor

Ahajha commented Aug 28, 2025

I have a PR that I would like merged here that would fix the recursive output structure, IMO it should be merged before CMake support is added. One of the things it does is collapses folders into files if the only contents are an __init__.py.

@laggykiller
Copy link
Contributor

laggykiller commented Aug 28, 2025

I finally remember why generating *_stub.tmp is necessary in my PR

NB_STUBGEN_OUTPUTS might be empty, causing OUTPUT to be empty, adding *_stub.tmp allows add_custom_command without OUTPUT.

See https://stackoverflow.com/questions/43347087/cmake-add-custom-command-without-output for more info

I have updated my PR (#463) to merge latest changes in upstream master and rename *_stub.tmp to ${name}_stub.stamp to better convey intention.


This PR solution might fail if output directory name is empty string

I have tried this PR by modifying self-tests in nanobind:

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 3bd5478..56866f2 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -182,6 +182,7 @@ set(TEST_FILES
   test_enum_ext.pyi.ref
   test_typing_ext.pyi.ref
   py_stub_test.py
+  py_stub_submodule_test.py
   py_stub_test.pyi.ref
 )
 
@@ -203,7 +204,9 @@ endif()
 nanobind_add_stub(
   py_stub
   MODULE py_stub_test
-  OUTPUT ${PYI_PREFIX}py_stub_test.pyi
+  OUTPUT ${PYI_PREFIX}
+  RECURSIVE
+  VERBOSE
   PYTHON_PATH $<TARGET_FILE_DIR:test_stl_ext>
   DEPENDS ${PY_STUB_TEST}
 )
diff --git a/tests/py_stub_test.py b/tests/py_stub_test.py
index c0251ba..39ba1f8 100644
--- a/tests/py_stub_test.py
+++ b/tests/py_stub_test.py
@@ -12,6 +12,8 @@ from os import PathLike, getcwd
 
 del sys
 
+from py_stub_submodule_test import BClass
+
 C = 123
 T = typing.TypeVar("T")
 
@@ -66,3 +68,5 @@ class AClass:
 
     def overloaded_2(self, x):
         "docstr 3"
+
+b = BClass(1)

Run tests with cmake -S . -B build -DNB_TEST_STABLE_ABI=ON -DNB_TEST_SHARED_BUILD="$(python -c 'import sys; print(int(sys.version_info.minor>=11))')" && cmake --build build -j

This would fail as ${PYI_PREFIX} is empty

@keltonhalbert
Copy link

Merging #1082 would also be super helpful, since right now I’m doing some work to try and coerce the current structure into my project. I’m currently having to rename my main module to init.pyi too, among other post processing steps (like setting return types to NDArray instead of ArrayLike).

@Ahajha
Copy link
Contributor

Ahajha commented Aug 28, 2025

Iirc my PR also adds another folder level, I wrote the motivation in the PR, essentially we don't want any surprise renaming. That should handle the empty prefix case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants