Skip to content

Commit

Permalink
Special forms for nb::typed
Browse files Browse the repository at this point in the history
  • Loading branch information
oremanj committed Dec 24, 2024
1 parent b7c4f1a commit 57c6a91
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 15 deletions.
21 changes: 20 additions & 1 deletion docs/api_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3099,7 +3099,8 @@ Miscellaneous
from the signature. To make this explicit, use the ``nb::typed<T, Ts...>``
wrapper to pass additional type parameters. This has no effect besides
clarifying the signature---in particular, nanobind does *not* insert
additional runtime checks!
additional runtime checks! At runtime, a ``nb::typed<T, Ts...>`` behaves
exactly like a ``T``.

.. code-block:: cpp
Expand All @@ -3108,3 +3109,21 @@ Miscellaneous
// ...
}
});
``nb::typed<nb::object, T>`` and ``nb::typed<nb::handle, T>`` are
treated specially: they generate a signature that refers just to ``T``,
rather than to the nonsensical ``object[T]`` that would otherwise
be produced. This can be useful if you want to replace the type of
a parameter instead of augmenting it. Note that at runtime these
perform no checks at all, since ``nb::object`` and ``nb::handle``
can refer to any Python object.

To support callable types, you can specify a C++ function signature in
``nb::typed<nb::callable, Sig>`` and nanobind will attempt to convert
it to a Python callable signature.
``nb::typed<nb::callable, int(float, std::string)>`` becomes
``Callable[[float, str], int]``, while
``nb::typed<nb::callable, int(...)>`` becomes ``Callable[..., int]``.
Type checkers will verify that any callable passed for such an argument
has a compatible signature. (At runtime, any sort of callable object
will be accepted.)
13 changes: 13 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ case, both modules must use the same nanobind ABI version, or they will be
isolated from each other. Releases that don't explicitly mention an ABI version
below inherit that of the preceding release.

Version TBD (not yet released)
------------------------------

- Added some special forms for :cpp:class:`nb::typed\<T, Ts...\> <typed>`:

- ``nb::typed<nb::object, T>`` or ``nb::typed<nb::object, T>`` produces
a parameter or return value that will be described like ``T`` in function
signatures but accepts any Python object at runtime

- ``nb::typed<nb::callable, R(Args...)>`` produces a Python callable signature
``Callable[[Args...], R]``; similarly, ``nb::typed<nb::callable, R(...)>``
(with a literal ellipsis) produces the Python ``Callable[..., R]``

Version 2.4.0 (Dec 6, 2024)
---------------------------

Expand Down
27 changes: 24 additions & 3 deletions docs/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ the signature of a default argument, the more targeted
:cpp:func:`nb::arg("name").sig("signature") <arg::sig>` annotation is
preferable to :cpp:class:`nb::sig <sig>`.

.. _typing_signature_classes:

Classes
^^^^^^^

Expand Down Expand Up @@ -178,7 +176,30 @@ subclasses the type ``T`` and can be used interchangeably with ``T``. The other
arguments (``Ts...``) are used to generate a Python type signature but have no
other effect (for example, parameterizing by ``str`` on the Python end can
alternatively be achieved by passing ``nb::str``, ``std::string``, or ``const
char*`` as part of the ``Ts..`` parameter pack).
char*`` as part of the ``Ts...`` parameter pack).

There are two special forms of ``nb::typed<T, Ts...>`` that will be rendered
as something other than ``T[Ts...]``:

* In some cases, a function may wish to accept or return an arbitrary
Python object, but generate signatures that describe it as some more
specific type ``T``. The types ``nb::typed<nb::object, T>`` and
``nb::typed<nb::handle, T>`` will be rendered as ``T`` rather than
as the nonsensical ``object[T]`` that they would be without this rule.
(If you want nanobind to check that an argument is actually of type ``T``,
while still giving you a generic Python object to work with,
then use :cpp:class:`nb::handle_t\<T\> <handle_t>` instead.)

* Type parameters for ``nb::callable`` can be provided using a C++ function
signature, since there would otherwise be no way to express the nested
brackets used in Python callable signatures. In order to express the Python type
``Callable[[str, float], int]``, which is a function taking two parameters
(string and float) and returning an integer, you might write
``nb::typed<nb::callable, int(nb::str, float)>``. For a callable type
that accepts any arguments, like ``Callable[..., int]``, use a C-style
variadic function signature: ``nb::typed<nb::callable, int(...)>``.
(The latter could also be written without this special support, as
``nb::typed<nb::callable, nb::ellipsis, int>``.)

.. _typing_generics_creating:

Expand Down
44 changes: 39 additions & 5 deletions include/nanobind/nb_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,13 @@ template <typename T> struct type_caster<pointer_and_handle<T>> {
}
};

template <typename T> struct typed_name {
template <typename T> struct typed_base_name {
static constexpr auto Name = type_caster<T>::Name;
};

#if PY_VERSION_HEX < 0x03090000
#define NB_TYPED_NAME_PYTHON38(type, name) \
template <> struct typed_name<type> { \
template <> struct typed_base_name<type> { \
static constexpr auto Name = detail::const_name(name); \
};

Expand All @@ -339,13 +339,47 @@ NB_TYPED_NAME_PYTHON38(dict, NB_TYPING_DICT)
NB_TYPED_NAME_PYTHON38(type_object, NB_TYPING_TYPE)
#endif

// Base case: typed<T, Ts...> renders as T[Ts...], with some adjustments to
// T for older versions of Python (typing.List instead of list, for example)
template <typename T, typename... Ts> struct typed_name {
static constexpr auto Name =
typed_base_name<intrinsic_t<T>>::Name + const_name("[") +
concat(const_name<std::is_same_v<Ts, ellipsis>>(const_name("..."),
make_caster<Ts>::Name)...) + const_name("]");
};

// typed<object, T> or typed<handle, T> renders as T, rather than as
// the nonsensical object[T]
template <typename T> struct typed_name<object, T> {
static constexpr auto Name = make_caster<T>::Name;
};
template <typename T> struct typed_name<handle, T> {
static constexpr auto Name = make_caster<T>::Name;
};

// typed<callable, R(Args...)> renders as Callable[[Args...], R]
template <typename R, typename... Args>
struct typed_name<callable, R(Args...)> {
using Ret = std::conditional_t<std::is_void_v<R>, void_type, R>;
static constexpr auto Name =
const_name(NB_TYPING_CALLABLE "[[") +
concat(make_caster<Args>::Name...) + const_name("], ") +
make_caster<Ret>::Name + const_name("]");
};
// typed<callable, R(...)> renders as Callable[..., R]
template <typename R>
struct typed_name<callable, R(...)> {
using Ret = std::conditional_t<std::is_void_v<R>, void_type, R>;
static constexpr auto Name =
const_name(NB_TYPING_CALLABLE "[..., ") +
make_caster<Ret>::Name + const_name("]");
};

template <typename T, typename... Ts> struct type_caster<typed<T, Ts...>> {
using Caster = make_caster<T>;
using Typed = typed<T, Ts...>;

NB_TYPE_CASTER(Typed, typed_name<intrinsic_t<T>>::Name + const_name("[") +
concat(const_name<std::is_same_v<Ts, ellipsis>>(const_name("..."),
make_caster<Ts>::Name)...) + const_name("]"))
NB_TYPE_CASTER(Typed, (typed_name<T, Ts...>::Name))

bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
Caster caster;
Expand Down
12 changes: 9 additions & 3 deletions tests/test_functions.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <string.h>

#include <nanobind/nanobind.h>
#include <nanobind/stl/function.h>
#include <nanobind/stl/pair.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
Expand Down Expand Up @@ -142,11 +143,16 @@ NB_MODULE(test_functions_ext, m) {
m.def("test_bad_tuple", []() { struct Foo{}; return nb::make_tuple("Hello", Foo()); });

/// Perform a Python function call from C++
m.def("test_call_1", [](nb::object o) { return o(1); });
m.def("test_call_2", [](nb::object o) { return o(1, 2); });
m.def("test_call_1", [](nb::typed<nb::object, std::function<int(int)>> o) {
return o(1);
});
m.def("test_call_2", [](nb::typed<nb::callable, void(int, int)> o) {
return o(1, 2);
});

/// Test expansion of args/kwargs-style arguments
m.def("test_call_extra", [](nb::object o, nb::args args, nb::kwargs kwargs) {
m.def("test_call_extra", [](nb::typed<nb::callable, void(...)> o,
nb::args args, nb::kwargs kwargs) {
return o(1, 2, *args, **kwargs, "extra"_a = 5);
});

Expand Down
6 changes: 3 additions & 3 deletions tests/test_functions_ext.pyi.ref
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,11 @@ def test_bytearray_resize(arg0: bytearray, arg1: int, /) -> None: ...

def test_bytearray_size(arg: bytearray, /) -> int: ...

def test_call_1(arg: object, /) -> object: ...
def test_call_1(arg: Callable[[int], int], /) -> object: ...

def test_call_2(arg: object, /) -> object: ...
def test_call_2(arg: Callable[[int, int], None], /) -> object: ...

def test_call_extra(arg0: object, /, *args, **kwargs) -> object: ...
def test_call_extra(arg0: Callable[..., None], /, *args, **kwargs) -> object: ...

def test_call_guard() -> int: ...

Expand Down

0 comments on commit 57c6a91

Please sign in to comment.