Skip to content

Commit

Permalink
Added nb::arg("..").sig("..") API, renamed nb::signature to `…
Browse files Browse the repository at this point in the history
…`nb::sig``

This commit adds a new interface to tweak how nanobind renders a single
default argument value in a function call. This is more targeted than
replacing the function's entire signature.

This commit also renames ``nb::signature`` to a shorter form
(``nb::sig``). I expect this to be more convenient since typed codebases
might require relatively frequent use of this annotation.
  • Loading branch information
wjakob committed Feb 25, 2024
1 parent 0eba2f4 commit df693fc
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 70 deletions.
21 changes: 14 additions & 7 deletions docs/api_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,13 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
Set a flag noting that implicit conversion should never be performed for
this function argument.

.. cpp:function:: arg &sig(const char * sig)

Override the signature of the default argument value. This is useful when
the argument value is unusually complex so that the default method to
explain it in docstrings and stubs (``str(value)``) does not produce
acceptable output.

.. cpp:struct:: is_method

Indicate that the bound function is a method.
Expand Down Expand Up @@ -1592,9 +1599,9 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
The ``Nurse`` and ``Patient`` annotation always refer to the *final* object
following implicit conversion.

.. cpp:struct:: signature
.. cpp:struct:: sig

.. cpp:function:: signature(const char * value)
.. cpp:function:: sig(const char * value)

This is *both* a class and a function binding annotation.

Expand All @@ -1609,7 +1616,7 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
.. code-block:: cpp
nb::def("function_name", &function_name,
nb::signature(
nb::sig(
"@decorator(decorator_args..)\n
"def function_name(arg_1: type_1 = def_1, ...) -> ret"
));
Expand All @@ -1625,7 +1632,7 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
.. code-block:: cpp
nb::class_<Class>(m, "Class",
nb::signature(
nb::sig(
"@decorator(decorator_args..)\n"
"class Class(Base1[T], Base2, meta=Meta)"
));
Expand Down Expand Up @@ -1736,8 +1743,8 @@ parameter of :cpp:func:`module_::def`, :cpp:func:`class_::def`,
nb::class_<MyClass>(m, "MyClass")
.def_rw("value", &MyClass::value,
nb::for_getter(nb::signature("def value(self, /) -> int")),
nb::for_setter(nb::signature("def value(self, value: int, /) -> None")),
nb::for_getter(nb::sig("def value(self, /) -> int")),
nb::for_setter(nb::sig("def value(self, value: int, /) -> None")),
nb::for_getter("docstring for getter"),
nb::for_setter("docstring for setter"));
Expand All @@ -1755,7 +1762,7 @@ Class binding annotations
The following annotations can be specified using the variable-length ``Extra``
parameter of the constructor :cpp:func:`class_::class_`.

Besides the below options, also refer to the :cpp:class:`signature` which is
Besides the below options, also refer to the :cpp:class:`sig` which is
usable in both function and class bindings. It can be used to override class
declarations in generated :ref:`stubs <stubs>`,

Expand Down
14 changes: 10 additions & 4 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,18 @@ This required work on three fronts:
produces a *generic* type that can be parameterized in Python (e.g.
``MyType[int]``). :ref:`Read more <typing_generics_creating>`.

* The :cpp:class:`nb::signature <signature>` annotation overrides the
* The :cpp:class:`nb::sig <sig>` annotation overrides the
signature of a function or method, e.g.:

.. code-block:: cpp
m.def("f", &f, nb::signature("def f(x: Foo = Foo(0)) -> None"), "docstring");
m.def("f", &f, nb::sig("def f(x: Foo = Foo(0)) -> None"), "docstring");
Each binding of an overloaded function can be customized separately. This
feature can be used to add decorators or control how default arguments are
rendered. :ref:`Read more <typing_signature_functions>`.

* The :cpp:class:`nb::signature <signature>` annotation can also override
* The :cpp:class:`nb::sig <sig>` annotation can also override
*class signatures* in generated stubs. Stubs often take certain liberties in
deviating somewhat from the precise type signature of the underlying
implementation, which is fine as long as this improves the capabilities of
Expand All @@ -102,7 +102,7 @@ This required work on three fronts:
using IntVec = std::vector<int>;
nb::class_<IntVec>(m, "IntVec",
nb::signature("class IntVec(Iterable[int])"));
nb::sig("class IntVec(Iterable[int])"));
Nanobind class bindings can't actually extend Python types, so this is a
convenient lie. Such shenanigans are worthwhile because they can greatly
Expand All @@ -115,6 +115,12 @@ This required work on three fronts:
function binding annotations (e.g., signature overrides) specifically to
the setter or the getter part of a property.

* The :cpp:class:`nb::arg("name") <arg>` argument annotation (and
``"name"_a`` shorthand) now have a :cpp:func:`.sig("signature")
<arg::sig>` member to control how a default value is rendered in the stubs
and docstrings. This provides more targeted control compared to overriding
the entire function signature.

Most importantly, it was possible to support these improvements with minimal
changes to the core parts of nanobind.

Expand Down
12 changes: 11 additions & 1 deletion docs/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ prior instantiation of ``nb::class_<SomeType>``), or an exception will be
thrown.

The "preview" of the default argument in the function signature is generated
using the object's ``__repr__`` method. If not available, the signature may not
using the object's ``__str__`` method. If not available, the signature may not
be very helpful, e.g.:

.. code-block:: pycon
Expand All @@ -44,6 +44,16 @@ be very helpful, e.g.:
| f(self, value: my_ext.SomeType = <my_ext.SomeType object at 0x1004d7230>) -> None
In such cases, you can either refine the implementation of the type in question
or manually override how nanobind renders the default value using the
:cpp:func:`.sig("string") method <arg::sig>`:

.. code-block:: cpp
nb::class_<MyClass>(m, "MyClass")
.def("f", &MyClass::f, "value"_a.sig("SomeType(123)") = SomeType(123));
.. _noconvert:

Implicit conversions, and how to suppress them
Expand Down
12 changes: 6 additions & 6 deletions docs/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ In this case, a static type checker like `MyPy
error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override]
To handle such cases, you can use the :cpp:class:`nb::signature <signature>`
To handle such cases, you can use the :cpp:class:`nb::sig <sig>`
attribute to overrides the function signature with a custom string.

.. code-block:: cpp
nb::class_<Int>(m, "Int")
.def(nb::self == nb::self,
nb::signature("def __eq__(self, arg: object, /) -> bool"));
nb::sig("def __eq__(self, arg: object, /) -> bool"));
The argument must be a valid Python function signature of the form ``def
name(...) -> ...`` without trailing colon (``":"``) and newlines, where
Expand Down Expand Up @@ -108,7 +108,7 @@ Here, we could specify
.. code-block:: cpp
nb::class_<IntVec>(m, "IntVec",
nb::signature("class IntVec(collections.abc.Iterable[int])"));
nb::sig("class IntVec(collections.abc.Iterable[int])"));
This is technically a lie. Such shenanigans are worthwhile because they can
greatly improve the development experience (e.g. `VS Code
Expand Down Expand Up @@ -225,11 +225,11 @@ This looks as follows:
// 2. Create a generic type, and indicate in generated stubs
// that it derives from Generic[T]
nb::class_<Wrapper>(m, "Wrapper", nb::is_generic(),
nb::signature("class Wrapper(typing.Generic[T])"))
nb::sig("class Wrapper(typing.Generic[T])"))
.def(nb::init<nb::object>(),
nb::signature("def __init__(self, arg: T, /) -> None"))
nb::sig("def __init__(self, arg: T, /) -> None"))
.def("get", [](Wrapper &w) { return w.value; },
nb::signature("def get(self, /) -> T"));
nb::sig("def get(self, /) -> T"));
This involves the following steps:

Expand Down
4 changes: 2 additions & 2 deletions docs/why.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ The following lists minor-but-useful additions relative to pybind11.
type signature of a class or function to provide richer type information to
static type checkers like `MyPy <https://github.com/python/mypy>`__ or
`PyRight <https://github.com/microsoft/pyright>`__. In such cases, specify
the :cpp:class:`nb::signature <signature>` attribute to override the default
the :cpp:class:`nb::sig <signature>` attribute to override the default
nanobind-provided signature.

For example, the following function signature annotation creates an overload
Expand All @@ -220,7 +220,7 @@ The following lists minor-but-useful additions relative to pybind11.
nb::raise("invalid input");
return arg;
},
nb::signature("def f(arg: typing.Literal[1], /) -> int"));
nb::sig("def f(arg: typing.Literal[1], /) -> int"));
Please see the section on :ref:`customizing function signatures
<typing_signature_functions>` and :ref:`class signatures
Expand Down
22 changes: 15 additions & 7 deletions include/nanobind/nb_attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct name {

struct arg_v;
struct arg {
NB_INLINE constexpr explicit arg(const char *name = nullptr) : name(name) {}
NB_INLINE constexpr explicit arg(const char *name = nullptr) : name_(name), signature_(nullptr) { }
template <typename T> NB_INLINE arg_v operator=(T &&value) const;
NB_INLINE arg &noconvert(bool value = true) {
convert_ = !value;
Expand All @@ -32,7 +32,12 @@ struct arg {
return *this;
}

const char *name;
NB_INLINE arg &sig(const char *value) {
signature_ = value;
return *this;
}

const char *name_, *signature_;
uint8_t convert_{ true };
bool none_{ false };
};
Expand Down Expand Up @@ -76,9 +81,9 @@ struct type_slots_callback {
cb_t callback;
};

struct signature {
struct sig {
const char *value;
signature(const char *doc) : value(doc) { }
sig(const char *value) : value(value) { }
};

struct is_getter { };
Expand Down Expand Up @@ -124,6 +129,7 @@ enum class func_flags : uint32_t {

struct arg_data {
const char *name;
const char *signature;
PyObject *name_py;
PyObject *value;
bool convert;
Expand Down Expand Up @@ -207,7 +213,7 @@ NB_INLINE void func_extra_apply(F &f, const scope &scope, size_t &) {
}

template <typename F>
NB_INLINE void func_extra_apply(F &f, const signature &s, size_t &) {
NB_INLINE void func_extra_apply(F &f, const sig &s, size_t &) {
f.flags |= (uint32_t) func_flags::has_signature;
f.name = s.value;
}
Expand Down Expand Up @@ -247,7 +253,8 @@ NB_INLINE void func_extra_apply(F &, nullptr_t, size_t &) { }
template <typename F>
NB_INLINE void func_extra_apply(F &f, const arg &a, size_t &index) {
arg_data &arg = f.args[index++];
arg.name = a.name;
arg.name = a.name_;
arg.signature = a.signature_;
arg.value = nullptr;
arg.convert = a.convert_;
arg.none = a.none_;
Expand All @@ -256,7 +263,8 @@ NB_INLINE void func_extra_apply(F &f, const arg &a, size_t &index) {
template <typename F>
NB_INLINE void func_extra_apply(F &f, const arg_v &a, size_t &index) {
arg_data &arg = f.args[index++];
arg.name = a.name;
arg.name = a.name_;
arg.signature = a.signature_;
arg.value = a.value.ptr();
arg.convert = a.convert_;
arg.none = a.none_;
Expand Down
2 changes: 1 addition & 1 deletion include/nanobind/nb_call.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ NB_INLINE void call_init(PyObject **args, PyObject *kwnames, size_t &nargs,
if constexpr (std::is_same_v<D, arg_v>) {
args[kwargs_offset + nkwargs] = value.value.release().ptr();
NB_TUPLE_SET_ITEM(kwnames, nkwargs++,
PyUnicode_InternFromString(value.name));
PyUnicode_InternFromString(value.name_));
} else if constexpr (std::is_same_v<D, args_proxy>) {
for (size_t i = 0, l = len(value); i < l; ++i)
args[nargs++] = borrow(value[i]).release().ptr();
Expand Down
2 changes: 1 addition & 1 deletion include/nanobind/nb_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ NB_INLINE void type_extra_apply(type_init_data & t, is_generic) {
t.flags |= (uint32_t) type_flags::is_generic;
}

NB_INLINE void type_extra_apply(type_init_data & t, const signature &s) {
NB_INLINE void type_extra_apply(type_init_data & t, const sig &s) {
t.flags |= (uint32_t) type_flags::has_signature;
t.name = s.value;
}
Expand Down
Loading

0 comments on commit df693fc

Please sign in to comment.