Skip to content

Commit f870315

Browse files
virtualdrwgkpre-commit-ci[bot]
authored
Provide better type hints for a variety of generic types (#4259)
* Provide better type hints for a variety of generic types * Makes better documentation * tuple, dict, list, set, function * Move to py::typing * style: pre-commit fixes * Update copyright line with correct year and actual author. The author information was copy-pasted from the git log output. --------- Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9ad7e82 commit f870315

File tree

6 files changed

+174
-1
lines changed

6 files changed

+174
-1
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ set(PYBIND11_HEADERS
141141
include/pybind11/stl.h
142142
include/pybind11/stl_bind.h
143143
include/pybind11/stl/filesystem.h
144-
include/pybind11/type_caster_pyobject_ptr.h)
144+
include/pybind11/type_caster_pyobject_ptr.h
145+
include/pybind11/typing.h)
145146

146147
# Compare with grep and warn if mismatched
147148
if(PYBIND11_MASTER_PROJECT AND NOT CMAKE_VERSION VERSION_LESS 3.12)

docs/advanced/misc.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,32 @@ before they are used as a parameter or return type of a function:
398398
pyFoo.def(py::init<const ns::Bar&>());
399399
pyBar.def(py::init<const ns::Foo&>());
400400
}
401+
402+
Setting inner type hints in docstrings
403+
======================================
404+
405+
When you use pybind11 wrappers for ``list``, ``dict``, and other generic python
406+
types, the docstring will just display the generic type. You can convey the
407+
inner types in the docstring by using a special 'typed' version of the generic
408+
type.
409+
410+
.. code-block:: cpp
411+
412+
PYBIND11_MODULE(example, m) {
413+
m.def("pass_list_of_str", [](py::typing::List<py::str> arg) {
414+
// arg can be used just like py::list
415+
));
416+
}
417+
418+
The resulting docstring will be ``pass_list_of_str(arg0: list[str]) -> None``.
419+
420+
The following special types are available in ``pybind11/typing.h``:
421+
422+
* ``py::Tuple<Args...>``
423+
* ``py::Dict<K, V>``
424+
* ``py::List<V>``
425+
* ``py::Set<V>``
426+
* ``py::Callable<Signature>``
427+
428+
.. warning:: Just like in python, these are merely hints. They don't actually
429+
enforce the types of their contents at runtime or compile time.

include/pybind11/typing.h

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
pybind11/typing.h: Convenience wrapper classes for basic Python types
3+
with more explicit annotations.
4+
5+
Copyright (c) 2023 Dustin Spicuzza <[email protected]>
6+
7+
All rights reserved. Use of this source code is governed by a
8+
BSD-style license that can be found in the LICENSE file.
9+
*/
10+
11+
#pragma once
12+
13+
#include "detail/common.h"
14+
#include "cast.h"
15+
#include "pytypes.h"
16+
17+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
18+
PYBIND11_NAMESPACE_BEGIN(typing)
19+
20+
/*
21+
The following types can be used to direct pybind11-generated docstrings
22+
to have have more explicit types (e.g., `list[str]` instead of `list`).
23+
Just use these in place of existing types.
24+
25+
There is no additional enforcement of types at runtime.
26+
*/
27+
28+
template <typename... Types>
29+
class Tuple : public tuple {
30+
using tuple::tuple;
31+
};
32+
33+
template <typename K, typename V>
34+
class Dict : public dict {
35+
using dict::dict;
36+
};
37+
38+
template <typename T>
39+
class List : public list {
40+
using list::list;
41+
};
42+
43+
template <typename T>
44+
class Set : public set {
45+
using set::set;
46+
};
47+
48+
template <typename Signature>
49+
class Callable;
50+
51+
template <typename Return, typename... Args>
52+
class Callable<Return(Args...)> : public function {
53+
using function::function;
54+
};
55+
56+
PYBIND11_NAMESPACE_END(typing)
57+
58+
PYBIND11_NAMESPACE_BEGIN(detail)
59+
60+
template <typename... Types>
61+
struct handle_type_name<typing::Tuple<Types...>> {
62+
static constexpr auto name
63+
= const_name("tuple[") + concat(make_caster<Types>::name...) + const_name("]");
64+
};
65+
66+
template <>
67+
struct handle_type_name<typing::Tuple<>> {
68+
// PEP 484 specifies this syntax for an empty tuple
69+
static constexpr auto name = const_name("tuple[()]");
70+
};
71+
72+
template <typename K, typename V>
73+
struct handle_type_name<typing::Dict<K, V>> {
74+
static constexpr auto name = const_name("dict[") + make_caster<K>::name + const_name(", ")
75+
+ make_caster<V>::name + const_name("]");
76+
};
77+
78+
template <typename T>
79+
struct handle_type_name<typing::List<T>> {
80+
static constexpr auto name = const_name("list[") + make_caster<T>::name + const_name("]");
81+
};
82+
83+
template <typename T>
84+
struct handle_type_name<typing::Set<T>> {
85+
static constexpr auto name = const_name("set[") + make_caster<T>::name + const_name("]");
86+
};
87+
88+
template <typename Return, typename... Args>
89+
struct handle_type_name<typing::Callable<Return(Args...)>> {
90+
using retval_type = conditional_t<std::is_same<Return, void>::value, void_type, Return>;
91+
static constexpr auto name = const_name("Callable[[") + concat(make_caster<Args>::name...)
92+
+ const_name("], ") + make_caster<retval_type>::name
93+
+ const_name("]");
94+
};
95+
96+
PYBIND11_NAMESPACE_END(detail)
97+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"include/pybind11/stl.h",
4545
"include/pybind11/stl_bind.h",
4646
"include/pybind11/type_caster_pyobject_ptr.h",
47+
"include/pybind11/typing.h",
4748
}
4849

4950
detail_headers = {

tests/test_pytypes.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
BSD-style license that can be found in the LICENSE file.
88
*/
99

10+
#include <pybind11/typing.h>
11+
1012
#include "pybind11_tests.h"
1113

1214
#include <utility>
@@ -820,4 +822,12 @@ TEST_SUBMODULE(pytypes, m) {
820822
a >>= b;
821823
return a;
822824
});
825+
826+
m.def("annotate_tuple_float_str", [](const py::typing::Tuple<py::float_, py::str> &) {});
827+
m.def("annotate_tuple_empty", [](const py::typing::Tuple<> &) {});
828+
m.def("annotate_dict_str_int", [](const py::typing::Dict<py::str, int> &) {});
829+
m.def("annotate_list_int", [](const py::typing::List<int> &) {});
830+
m.def("annotate_set_str", [](const py::typing::Set<std::string> &) {});
831+
m.def("annotate_fn",
832+
[](const py::typing::Callable<int(py::typing::List<py::str>, py::str)> &) {});
823833
}

tests/test_pytypes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,3 +896,38 @@ def test_inplace_lshift(a, b):
896896
def test_inplace_rshift(a, b):
897897
expected = a >> b
898898
assert m.inplace_rshift(a, b) == expected
899+
900+
901+
def test_tuple_nonempty_annotations(doc):
902+
assert (
903+
doc(m.annotate_tuple_float_str)
904+
== "annotate_tuple_float_str(arg0: tuple[float, str]) -> None"
905+
)
906+
907+
908+
def test_tuple_empty_annotations(doc):
909+
assert (
910+
doc(m.annotate_tuple_empty) == "annotate_tuple_empty(arg0: tuple[()]) -> None"
911+
)
912+
913+
914+
def test_dict_annotations(doc):
915+
assert (
916+
doc(m.annotate_dict_str_int)
917+
== "annotate_dict_str_int(arg0: dict[str, int]) -> None"
918+
)
919+
920+
921+
def test_list_annotations(doc):
922+
assert doc(m.annotate_list_int) == "annotate_list_int(arg0: list[int]) -> None"
923+
924+
925+
def test_set_annotations(doc):
926+
assert doc(m.annotate_set_str) == "annotate_set_str(arg0: set[str]) -> None"
927+
928+
929+
def test_fn_annotations(doc):
930+
assert (
931+
doc(m.annotate_fn)
932+
== "annotate_fn(arg0: Callable[[list[str], str], int]) -> None"
933+
)

0 commit comments

Comments
 (0)