wjakob / nanobind

nanobind: tiny and efficient C++/Python bindings
BSD 3-Clause "New" or "Revised" License
2.14k stars 161 forks source link

[BUG]: spurious import added to generated stubs based on function `m.def` nb::sig() field contents #559

Closed nurpax closed 2 months ago

nurpax commented 2 months ago

Problem description

Python 3.11, Nanobind master from a few days ago.

I define a function binding like so:

m.def("is_item_clicked", [](ImGuiMouseButton mouse_button) {
    return ImGui::IsItemClicked(mouse_button);
}, nb::sig("def is_item_clicked(mouse_button: MouseButton | int = MouseButton.LEFT) -> bool"), "mouse_button"_a = 0);

nb::sig() contents MouseButton | int and/or default argument MouseButton.LEFT cause the output stubs to include an import for MouseButton:

from collections.abc import Iterator, Sequence
import enum
from typing import overload

import MouseButton  # <---- this line here

class BackendFlags(enum.IntEnum):
    _new_member_ = __new__

If I define the API bindging as (without a reference to MouseButton):

m.def("is_item_clicked", [](ImGuiMouseButton mouse_button) {
    return ImGui::IsItemClicked(mouse_button);
}, nb::sig("def is_item_clicked(mouse_button: int = 0) -> bool"), "mouse_button"_a = 0);

the import disappears and the start of the generated stub looks like this:

from collections.abc import Iterator, Sequence
import enum
from typing import overload

class BackendFlags(enum.IntEnum):
    _new_member_ = __new__

MouseButton is an enum in my bindings and its stub looks like this:

class MouseButton(enum.IntEnum):
    _new_member_ = __new__
    _use_args_: bool = True
    _member_names_: list = ['LEFT', 'RIGHT', 'MIDDLE', 'COUNT']
    _member_map_: dict = ...
    _value2member_map_: dict = ...
    _unhashable_values_: list = []
    _value_repr_ = __repr__

    LEFT: MouseButton
    RIGHT: MouseButton
    MIDDLE: MouseButton
    COUNT: MouseButton

Reproducible example code

  1. Clone https://github.com/wjakob/nanobind_example
  2. Modify the example as below.
  3. Build project and generate stubs with python -m nanobind.stubgen -m nanobind_example.nanobind_example_ext -o out.pyi

Modified nanobind_example code:

#include <nanobind/nanobind.h>

namespace nb = nanobind;

using namespace nb::literals;

enum ImGuiMouseButton_
{
    ImGuiMouseButton_Left = 0,
    ImGuiMouseButton_Right = 1,
    ImGuiMouseButton_Middle = 2,
    ImGuiMouseButton_COUNT = 5
};

NB_MODULE(nanobind_example_ext, m) {
    nb::enum_<ImGuiMouseButton_>(m, "MouseButton", nb::is_arithmetic())
        .value("LEFT", ImGuiMouseButton_Left)
        .value("RIGHT", ImGuiMouseButton_Right)
        .value("MIDDLE", ImGuiMouseButton_Middle)
        .value("COUNT", ImGuiMouseButton_COUNT);

    m.def("add", [](int a, int b) { return a + b; }, "a"_a, "b"_a);
    m.def("repro", [](int button) { 
        return button; 
    }, nb::sig("def repro(button: MouseButton | int = MouseButton.LEFT) -> bool"), "button"_a = 0);
}

The output stub looks like this:

import enum

import MouseButton

class MouseButton(enum.IntEnum):
    LEFT: MouseButton

    RIGHT: MouseButton

    MIDDLE: MouseButton

    COUNT: MouseButton

def add(a: int, b: int) -> int: ...

def repro(button: MouseButton | int = MouseButton.LEFT) -> bool: ...
nurpax commented 2 months ago

Expected behavior: I don't expect the contents of nb::sigs to add new imports to generated .pyi files.

wjakob commented 2 months ago

The stub generator expects fully namespace-qualified type names. You must add the module name in front.

wjakob commented 2 months ago

Did you add some flag to the stub generator that it should include private bits? I'm surprised that it shows enum members like _member_names_

nurpax commented 2 months ago

Did you add some flag to the stub generator that it should include private bits?

I have been wondering where and why these are coming from. Maybe it's because I use INCLUDE_PRIVATE in my cmake config:

nanobind_add_stub(
  slimgui_ext_stub
  MODULE slimgui_ext
  OUTPUT "${CMAKE_SOURCE_DIR}/src/slimgui/slimgui_ext.pyi"
  PYTHON_PATH $<TARGET_FILE_DIR:slimgui_ext>
  DEPENDS slimgui_ext
  INCLUDE_PRIVATE
)

I use this option because ImGui includes enums that have a trailing underscore such as this INVALID_MASK_ below:

nb::enum_<ImGuiSliderFlags_>(m, "SliderFlags", nb::is_arithmetic())
    .value("NONE", ImGuiSliderFlags_None)
    .value("ALWAYS_CLAMP", ImGuiSliderFlags_AlwaysClamp)
    .value("LOGARITHMIC", ImGuiSliderFlags_Logarithmic)
    .value("NO_ROUND_TO_FORMAT", ImGuiSliderFlags_NoRoundToFormat)
    .value("NO_INPUT", ImGuiSliderFlags_NoInput)
    .value("INVALID_MASK_", ImGuiSliderFlags_InvalidMask_);

The stub generator expects fully namespace-qualified type names

OK, I'll try that! EDIT: it worked! Closing the bug.