Skip to content

Commit 5ab4b9c

Browse files
committed
Replace nb::typed with a more intuitive interface
1 parent 624b63c commit 5ab4b9c

File tree

13 files changed

+191
-142
lines changed

13 files changed

+191
-142
lines changed

docs/api_core.rst

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,13 +1064,6 @@ Wrapper classes
10641064

10651065
Iterator inequality comparison operator.
10661066

1067-
.. cpp:class:: template <typename T> iterator_t : public iterator
1068-
1069-
Type-parameterized form of the iterator class. Note that the use of this
1070-
class does not enable any kind of type checking within nanobind. It is
1071-
mainly useful to annotate arguments and return values so that they are
1072-
rendered as ``collections.abc.Iterator[T]`` in typing :ref:`stubs <stubs>`.
1073-
10741067
.. cpp:class:: iterable : public object
10751068

10761069
Wrapper class representing an object that can be iterated upon (in the sense
@@ -2572,14 +2565,11 @@ Miscellaneous
25722565
is not available (e.g., for custom binding-specific constructors that don't
25732566
exist in `Target` type).
25742567

2575-
.. cpp:struct:: template <typename T, typename D> typed
2568+
.. cpp:class:: template <typename T, typename... Ts> typed
25762569

2577-
This helper class provides an an API for overriding the type
2578-
annotation of a function argument or return value in generated
2579-
docstrings. It is particularly helpful when the type signature is
2580-
not obvious and must be computed at compile time. Otherwise, the
2581-
:cpp:class:`signature` attribute provides a simpler alternative for
2582-
taking full control function type annotations.
2570+
This helper class provides an an interface to parameterize generic types to
2571+
improve generated Python function signatures (e.g., to turn ``list`` into
2572+
``list[MyType]``).
25832573

25842574
Consider the following binding that iterates over a Python list.
25852575

@@ -2591,36 +2581,16 @@ Miscellaneous
25912581
}
25922582
});
25932583
2594-
Suppose that ``f`` expects a list of ``Foo`` objects, which is not clear
2595-
from the signature. To improve the function signature, use the
2596-
``nb::typed<T, D>`` wrapper class to pass the argument.
2597-
2598-
The template argument `T` should be set to the original argument type, and
2599-
`D` points to a helper class that will be used to compute the type name at
2600-
compile time. Any access to the list ``l`` must be replaced by ``l.value``:
2584+
Suppose that ``f`` expects a list of ``MyType`` objects, which is not clear
2585+
from the signature. To make this explicit, use the ``nb::typed<T, Ts...>``
2586+
wrapper to pass additional type parameters. This has no effect besides
2587+
clarifying the signature---in particular, nanobind does *not* insert
2588+
additional runtime checks!
26012589

26022590
.. code-block:: cpp
26032591
2604-
m.def("f", [](nb::typed<nb::list, FooListName> l) {
2605-
for (nb::handle h : l.value) {
2592+
m.def("f", [](nb::typed<nb::list, MyType> l) {
2593+
for (nb::handle h : l) {
26062594
// ...
26072595
}
26082596
});
2609-
2610-
In this simple example, the ``FooListName`` type can be defined as follows:
2611-
2612-
.. code-block:: cpp
2613-
2614-
struct FooListName {
2615-
static constexpr auto Name =
2616-
nb::detail::const_name("list[") +
2617-
nb::detail::const_name<Foo>() +
2618-
nb::detail::const_name("]");
2619-
};
2620-
2621-
More generally, `D` can be a templated class with partial overloads,
2622-
which allows for advanced constructions.
2623-
2624-
.. cpp:member:: T value
2625-
2626-
Wrapped value of the `typed` parameter.

docs/api_extra.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ include directive:
373373
:cpp:class:`keep_alive` annotation is needed to tie the lifetime of the
374374
parent container to that of the iterator.
375375

376-
The return value is a typed iterator (:cpp:class:`iterator_t`), whose
377-
template parameter is given by the type of ``*first``.
376+
The return value is a typed iterator (:cpp:class:`iterator` wrapped using
377+
:cpp:class:`typed`), whose template parameter is given by the type of
378+
``*first``.
378379

379380
Here is an example of what this might look like for a STL vector:
380381

@@ -402,8 +403,9 @@ include directive:
402403
key-value pairs. `make_key_iterator` returns the first pair element to
403404
iterate over keys.
404405

405-
The return value is a typed iterator (:cpp:class:`iterator_t`), whose
406-
template parameter is given by the type of ``(*first).first``.
406+
The return value is a typed iterator (:cpp:class:`iterator` wrapped using
407+
:cpp:class:`typed`), whose template parameter is given by the type of
408+
``(*first).first``.
407409

408410

409411
.. cpp:function:: template <rv_policy Policy = rv_policy::reference_internal, typename Iterator, typename... Extra> iterator make_value_iterator(handle scope, const char * name, Iterator &&first, Iterator &&last, Extra &&...extra)
@@ -412,8 +414,9 @@ include directive:
412414
key-value pairs. `make_value_iterator` returns the second pair element to
413415
iterate over values.
414416

415-
The return value is a typed iterator (:cpp:class:`iterator_t`), whose
416-
template parameter is given by the type of ``(*first).second``.
417+
The return value is a typed iterator (:cpp:class:`iterator` wrapped using
418+
:cpp:class:`typed`), whose template parameter is given by the type of
419+
``(*first).second``.
417420

418421
N-dimensional array type
419422
------------------------

docs/changelog.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@ API and ABI level, requiring new major version according to `SemVer
3434

3535
- The ability to override the combined docstring and overload signature listing
3636
with a raw string (formerly ``nb::raw_doc``) was replaced with a more
37-
fine-grained annotation named :cpp:class:`nb::signature`.
37+
fine-grained annotation named :cpp:class:`nb::signature <signature>`.
3838

3939
The new interface enables changing the signature of individual overloads
4040
without touching the docstring part. This change was needed by the new stub
4141
generator. Existing use of ``nb::raw_doc`` must be reworked into this format,
42-
see :ref:`here <fsig_override>` for an example.
42+
see :ref:`here <fsig_override>` for an example. This is an API-breaking change.
43+
44+
- The behavior of the :cpp:class:`nb::typed\<T, Ts...\> <typed>` wrapper was
45+
changed to make this feature equivalent to parameterization of generic types
46+
in in Python -- for example
47+
48+
.. code-block:: cpp
49+
50+
m.def("f", [](nb::typed<nb::mapping, int, nb::str> list) { ... });
51+
52+
produces ``collections.abc.Mapping[int, str]`` in generated stubs.
53+
Previously, this feature required that the user provide a custom type
54+
formatting implementation, which was somewhat awkward to use. This is an
55+
API-breaking change.
4356

4457
- The release improves many type caster so that they produce more accurate type
4558
signatures. For example, the STL ``std::vector<T>`` type caster now renders
@@ -71,7 +84,7 @@ Version 1.9.2 (Feb 23, 2024)
7184

7285
* :cpp:func:`nb::try_cast() <try_cast>` no longer crashes the interpreter when
7386
attempting to cast a Python ``None`` to a C++ type that was bound using
74-
:cpp:class:`nb::class_<...> <class_>`. Previously this would raise an
87+
:cpp:class:`nb::class_\<...\> <class_>`. Previously this would raise an
7588
exception from the cast operator, which would result in a call to
7689
``std::terminate()`` because :cpp:func:`try_cast() <try_cast>` is declared
7790
``noexcept``. (PR `#386 <https://github.com/wjakob/nanobind/pull/386>`__).

docs/exchanging.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,7 @@ directives:
390390
:cpp:class:`dict`, :cpp:class:`ellipsis`, :cpp:class:`handle`,
391391
:cpp:class:`handle_t\<T\> <handle_t>`,
392392
:cpp:class:`bool_`, :cpp:class:`int_`, :cpp:class:`float_`,
393-
:cpp:class:`iterable`,
394-
:cpp:class:`iterator`,
395-
:cpp:class:`iterator_t`,
393+
:cpp:class:`iterable`, :cpp:class:`iterator`,
396394
:cpp:class:`list`, :cpp:class:`mapping`,
397395
:cpp:class:`module_`, :cpp:class:`object`, :cpp:class:`set`, :cpp:class:`sequence`,
398396
:cpp:class:`slice`, :cpp:class:`str`, :cpp:class:`tuple`,

docs/functions.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,3 +475,100 @@ The following interactive session shows how to call them from Python.
475475
This functionality is very useful when generating bindings for callbacks in
476476
C++ libraries (e.g. GUI libraries, asynchronous networking libraries,
477477
etc.).
478+
479+
.. _typing_generics:
480+
481+
Parameterizing generic types
482+
----------------------------
483+
484+
Various standard Python types are `generic
485+
<https://typing.readthedocs.io/en/latest/spec/generics.html>`__ can be
486+
parameterized to improve the effectiveness of static type checkers such as
487+
`MyPy <https://github.com/python/mypy>`__. In the presence of such a
488+
specialization, a type checker can, e.g., infer that the variable ``a`` below
489+
is of type ``int``.
490+
491+
.. code-block:: python
492+
493+
def f() -> list[int]: ...
494+
495+
a = f()[0]
496+
497+
This is even supported for *abstract types*---for example,
498+
``collections.abc.Mapping[str, int]`` indicates an abstract mapping from
499+
strings to integers.
500+
501+
nanobind provides the template class :cpp:class:`nb::typed\<T, Ts...\> <typed>`
502+
to generate parameterized type annotations in C++ bindings. For example, the
503+
argument and return value of the following function binding reproduces the
504+
exact list and mapping types mentioned above.
505+
506+
.. code-block:: cpp
507+
508+
m.def("f", [](nb::typed<nb::mapping, nb::str, int> arg)
509+
-> nb::typed<nb::list, int> { ... });
510+
511+
(Usually, :cpp:class:`nb::typed\<T, Ts...\> <typed>` would be applied to
512+
:ref:`wrapper <wrappers>` types, though this is not a strict limitation.)
513+
514+
An important limitation of this feature is that it *only* affects function
515+
signatures. Nanobind will (as always) ensure that ``f`` can only be called with
516+
a ``nb::mapping``, but it will *not* insert additional runtime to verify that
517+
``arg`` indeed maps strings to integers. It is the responsibility of the
518+
function to perform such checks at runtime and, if needed, raise a
519+
:cpp:func:`nb::type_error <type_error>`.
520+
521+
The parameterized C++ type :cpp:class:`nb::typed\<T, Ts...\> <typed>`
522+
subclasses the type ``T`` and can be used interchangeably with ``T``. The other
523+
arguments (``Ts...``) are used to generate a Python type signature but have no
524+
other effect (for example, a parameterizing by ``str`` on the Python end can
525+
alternatively be achieved by passing ``nb::str``, ``std::string``, or ``const
526+
char*`` as part of the ``Ts..`` parameter pack).
527+
528+
.. _typing_signatures:
529+
530+
Signature customization
531+
-----------------------
532+
533+
In larger binding projects, some customization of function signatures is often
534+
needed so that static type checkers accept the generated stubs. For example,
535+
the following function binding
536+
537+
.. code-block:: cpp
538+
539+
nb::class_<Int>(m, "Int")
540+
.def(nb::self == nb::self);
541+
542+
is likely to be rejected because the nanobind-derived ``__eq__`` function
543+
signature
544+
545+
.. code-block:: text
546+
547+
__eq__(self, arg: Int, /) -> bool
548+
549+
is more specific than that of the parent class ``object``:
550+
551+
.. code-block:: text
552+
553+
__eq__(self, arg: object, /) -> bool
554+
555+
In this case, `MyPy <https://github.com/python/mypy>`__, e.g., reports
556+
557+
.. code-block:: text
558+
559+
error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override]
560+
561+
To handle such cases, you can use the :cpp:class:`nb::signature <signature>`
562+
function binding attribute, which overrides the complete function signature
563+
with a custom string.
564+
565+
.. code-block:: cpp
566+
567+
nb::class_<Int>(m, "Int")
568+
.def(nb::self == nb::self,
569+
nb::signature("__eq__(self, arg: object, /) -> bool"));
570+
571+
Note that this *must* be a valid Python function signature of the form
572+
``name(...) -> ...``, where ``name`` must furthermore match the name given to
573+
the binding declaration (this comment applies to ``.def("name", ...)``-style
574+
bindings with explicit name).

docs/stubs.rst

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -218,47 +218,13 @@ Note that for now, the ``nanobind.stubgen.StubGen`` API is considered
218218
experimental and not subject to the semantic versioning policy used by the
219219
nanobind project.
220220

221-
Signature customization
222-
-----------------------
221+
Customizing type and function signatures
222+
----------------------------------------
223223

224224
In larger binding projects, some customization is often needed so that static
225-
type checkers accept the generated stubs. The stub produced by the following
226-
binding
225+
type checkers accept the generated stubs.
227226

228-
.. code-block:: cpp
227+
Please see the section on :ref:`overriding function signatures
228+
<typing_signatures>` and on :ref:`parameterizing generic types
229+
<typing_generics>` for ideas on this topic.
229230

230-
nb::class_<Int>(m, "Int")
231-
.def(nb::self == nb::self);
232-
233-
is likely rejected because the nanobind-derived ``__eq__`` function signature
234-
235-
.. code-block:: text
236-
237-
__eq__(self, arg: Int, /) -> bool
238-
239-
is more specific than that of the parent class ``object``:
240-
241-
.. code-block:: text
242-
243-
__eq__(self, arg: object, /) -> bool
244-
245-
In this case, MyPy, e.g., reports
246-
247-
.. code-block:: text
248-
249-
error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override]
250-
251-
To handle such cases, you can use the :cpp:class:`nb::signature <signature>`
252-
function binding attribute, which overrides the complete function signature
253-
with a custom string.
254-
255-
.. code-block:: cpp
256-
257-
nb::class_<Int>(m, "Int")
258-
.def(nb::self == nb::self,
259-
nb::signature("__eq__(self, arg: object, /) -> bool"));
260-
261-
Note that this *must* be a valid Python function signature of the form
262-
``name(...) -> ...``, where ``name`` must furthermore match the name given to
263-
the binding declaration (this comment applies to ``.def("name", ...)``-style
264-
bindings with explicit name).

docs/why.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ The following lists minor-but-useful additions relative to pybind11.
193193
overload when the underlying Python type object is a subtype of the C++ type
194194
``T``.
195195

196+
Finally, the :cpp:class:`nb::typed\<T, Ts...\> <typed>` annotation can be
197+
used to parameterize any other type. The feature exists to improve the
198+
expressiveness of type signatures (e.g., to turn ``list`` into
199+
``list[int]``). Note, however, that nanobind does not perform additional
200+
runtime checks in this case. Please see the section on :ref:`parameterizing
201+
generics <typing_generics>` for further details.
202+
196203
.. _fsig_override:
197204

198205
- **Function signature overrides**: it may sometimes be necessary to tweak the
@@ -204,8 +211,8 @@ The following lists minor-but-useful additions relative to pybind11.
204211

205212
For example, the following signature annotation creates an overload that
206213
should only be called with an ``1``-valued integer literal. While the
207-
function also includes a runtime-check, a type checker can now statically
208-
enforce this constraint.
214+
function also includes a runtime-check, a type checker can now already
215+
statically enforce this constraint.
209216

210217
.. code-block:: cpp
211218
@@ -216,3 +223,7 @@ The following lists minor-but-useful additions relative to pybind11.
216223
return arg;
217224
},
218225
nb::signature("f(arg: typing.Literal[1], /) -> int"));
226+
227+
Please see the section on :ref:`customizing function signatures
228+
<typing_signatures>` for further details.
229+

include/nanobind/make_iterator.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ template <typename Iterator> struct iterator_value_access {
4444

4545
template <typename Access, rv_policy Policy, typename Iterator,
4646
typename Sentinel, typename ValueType, typename... Extra>
47-
iterator_t<ValueType> make_iterator_impl(handle scope, const char *name,
48-
Iterator &&first, Sentinel &&last,
49-
Extra &&...extra) {
47+
typed<iterator, ValueType> make_iterator_impl(handle scope, const char *name,
48+
Iterator &&first, Sentinel &&last,
49+
Extra &&...extra) {
5050
using State = iterator_state<Access, Policy, Iterator, Sentinel, ValueType, Extra...>;
5151

5252
if (!type<State>().is_valid()) {
@@ -70,8 +70,8 @@ iterator_t<ValueType> make_iterator_impl(handle scope, const char *name,
7070
Policy);
7171
}
7272

73-
return borrow<iterator_t<ValueType>>(cast(State{ std::forward<Iterator>(first),
74-
std::forward<Sentinel>(last), true }));
73+
return borrow<typed<iterator, ValueType>>(cast(State{
74+
std::forward<Iterator>(first), std::forward<Sentinel>(last), true }));
7575
}
7676

7777
NAMESPACE_END(detail)

0 commit comments

Comments
 (0)