diff --git a/docs/api_core.rst b/docs/api_core.rst index 730f841f..197ceb17 100644 --- a/docs/api_core.rst +++ b/docs/api_core.rst @@ -3099,7 +3099,8 @@ Miscellaneous from the signature. To make this explicit, use the ``nb::typed`` 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`` behaves + exactly like a ``T``. .. code-block:: cpp @@ -3108,3 +3109,21 @@ Miscellaneous // ... } }); + + ``nb::typed`` and ``nb::typed`` 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`` and nanobind will attempt to convert + it to a Python callable signature. + ``nb::typed`` becomes + ``Callable[[float, str], int]``, while + ``nb::typed`` 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.) diff --git a/docs/changelog.rst b/docs/changelog.rst index cc364ab4..542e5f3f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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\ `: + + - ``nb::typed`` or ``nb::typed`` produces + a parameter or return value that will be described like ``T`` in function + signatures but accepts any Python object at runtime + + - ``nb::typed`` produces a Python callable signature + ``Callable[[Args...], R]``; similarly, ``nb::typed`` + (with a literal ellipsis) produces the Python ``Callable[..., R]`` + Version 2.4.0 (Dec 6, 2024) --------------------------- diff --git a/docs/typing.rst b/docs/typing.rst index 57bdbf0d..50790b67 100644 --- a/docs/typing.rst +++ b/docs/typing.rst @@ -80,8 +80,6 @@ the signature of a default argument, the more targeted :cpp:func:`nb::arg("name").sig("signature") ` annotation is preferable to :cpp:class:`nb::sig `. -.. _typing_signature_classes: - Classes ^^^^^^^ @@ -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`` 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`` and + ``nb::typed`` 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\ ` 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``. For a callable type + that accepts any arguments, like ``Callable[..., int]``, use a C-style + variadic function signature: ``nb::typed``. + (The latter could also be written without this special support, as + ``nb::typed``.) .. _typing_generics_creating: diff --git a/include/nanobind/nb_cast.h b/include/nanobind/nb_cast.h index 74ad6cef..5576304e 100644 --- a/include/nanobind/nb_cast.h +++ b/include/nanobind/nb_cast.h @@ -322,13 +322,13 @@ template struct type_caster> { } }; -template struct typed_name { +template struct typed_base_name { static constexpr auto Name = type_caster::Name; }; #if PY_VERSION_HEX < 0x03090000 #define NB_TYPED_NAME_PYTHON38(type, name) \ - template <> struct typed_name { \ + template <> struct typed_base_name { \ static constexpr auto Name = detail::const_name(name); \ }; @@ -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 renders as T[Ts...], with some adjustments to +// T for older versions of Python (typing.List instead of list, for example) +template struct typed_name { + static constexpr auto Name = + typed_base_name>::Name + const_name("[") + + concat(const_name>(const_name("..."), + make_caster::Name)...) + const_name("]"); +}; + +// typed or typed renders as T, rather than as +// the nonsensical object[T] +template struct typed_name { + static constexpr auto Name = make_caster::Name; +}; +template struct typed_name { + static constexpr auto Name = make_caster::Name; +}; + +// typed renders as Callable[[Args...], R] +template +struct typed_name { + using Ret = std::conditional_t, void_type, R>; + static constexpr auto Name = + const_name(NB_TYPING_CALLABLE "[[") + + concat(make_caster::Name...) + const_name("], ") + + make_caster::Name + const_name("]"); +}; +// typed renders as Callable[..., R] +template +struct typed_name { + using Ret = std::conditional_t, void_type, R>; + static constexpr auto Name = + const_name(NB_TYPING_CALLABLE "[..., ") + + make_caster::Name + const_name("]"); +}; + template struct type_caster> { using Caster = make_caster; using Typed = typed; - NB_TYPE_CASTER(Typed, typed_name>::Name + const_name("[") + - concat(const_name>(const_name("..."), - make_caster::Name)...) + const_name("]")) + NB_TYPE_CASTER(Typed, (typed_name::Name)) bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { Caster caster; diff --git a/tests/test_functions.cpp b/tests/test_functions.cpp index 45d182c9..66718737 100644 --- a/tests/test_functions.cpp +++ b/tests/test_functions.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -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> o) { + return o(1); + }); + m.def("test_call_2", [](nb::typed 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 o, + nb::args args, nb::kwargs kwargs) { return o(1, 2, *args, **kwargs, "extra"_a = 5); }); diff --git a/tests/test_functions_ext.pyi.ref b/tests/test_functions_ext.pyi.ref index 3a025ba1..773c3868 100644 --- a/tests/test_functions_ext.pyi.ref +++ b/tests/test_functions_ext.pyi.ref @@ -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: ...