Skip to content

Commit 26c9dc7

Browse files
committed
Compose literals for argument values in docstring
1 parent de78bdd commit 26c9dc7

File tree

4 files changed

+136
-2
lines changed

4 files changed

+136
-2
lines changed

include/pybind11/pybind11.h

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,73 @@ class cpp_function : public function {
119119
return new detail::function_record();
120120
}
121121

122+
template<typename ContainerHandle, typename StringifierUnary>
123+
static std::string join(ContainerHandle container, StringifierUnary&& f, std::string sep = ", ") {
124+
std::string joined;
125+
for (auto element : container) {
126+
joined += f(element) + sep;
127+
}
128+
if (!joined.empty()) {
129+
joined.erase(joined.size() - sep.size());
130+
}
131+
return joined;
132+
}
133+
134+
// Generate a literal expression for default function argument values
135+
static std::string compose_literal(pybind11::handle h) {
136+
auto typehandle = type::handle_of(h);
137+
if (detail::get_internals().registered_types_py.count(Py_TYPE(h.ptr())) > 0) {
138+
if (hasattr(typehandle, "__members__") && hasattr(h, "name")) {
139+
// Bound enum type, can be fully represented
140+
auto descr = typehandle.attr("__module__").cast<std::string>();
141+
descr += "." + typehandle.attr("__qualname__").cast<std::string>();
142+
descr += "." + h.attr("name").cast<std::string>();
143+
return descr;
144+
}
145+
146+
// Use ellipsis expression instead of repr to ensure syntactic validity
147+
return "...";
148+
}
149+
150+
if (isinstance<dict>(h)) {
151+
std::string literal = "{";
152+
literal += join(
153+
reinterpret_borrow<dict>(h),
154+
[](const std::pair<handle, handle>& v) { return compose_literal(v.first) + ": " + compose_literal(v.second); }
155+
);
156+
literal += "}";
157+
return literal;
158+
}
159+
160+
if (isinstance<list>(h)) {
161+
std::string literal = "[";
162+
literal += join(reinterpret_borrow<list>(h), &compose_literal);
163+
literal += "]";
164+
return literal;
165+
}
166+
167+
if (isinstance<tuple>(h)) {
168+
std::string literal = "(";
169+
literal += join(reinterpret_borrow<tuple>(h), &compose_literal);
170+
literal += ")";
171+
return literal;
172+
}
173+
174+
if (isinstance<set>(h)) {
175+
auto v = reinterpret_borrow<set>(h);
176+
if (v.empty()) {
177+
return "set()";
178+
}
179+
std::string literal = "{";
180+
literal += join(v, &compose_literal);
181+
literal += "}";
182+
return literal;
183+
}
184+
185+
// All other types should be terminal and well-represented by repr
186+
return repr(h).cast<std::string>();
187+
}
188+
122189
/// Special internal constructor for functors, lambda functions, etc.
123190
template <typename Func, typename Return, typename... Args, typename... Extra>
124191
void initialize(Func &&f, Return (*)(Args...), const Extra&... extra) {
@@ -235,8 +302,10 @@ class cpp_function : public function {
235302
a.name = strdup(a.name);
236303
if (a.descr)
237304
a.descr = strdup(a.descr);
238-
else if (a.value)
239-
a.descr = strdup(repr(a.value).cast<std::string>().c_str());
305+
else if (a.value) {
306+
std::string literal = compose_literal(a.value);
307+
a.descr = strdup(literal.c_str());
308+
}
240309
}
241310

242311
rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__");

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ set(PYBIND11_TEST_FILES
9797
test_copy_move.cpp
9898
test_custom_type_casters.cpp
9999
test_docstring_options.cpp
100+
test_docstring_function_signature.cpp
100101
test_eigen.cpp
101102
test_enum.cpp
102103
test_eval.cpp
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
tests/test_docstring_options.cpp -- generation of docstrings function signatures
3+
4+
All rights reserved. Use of this source code is governed by a
5+
BSD-style license that can be found in the LICENSE file.
6+
*/
7+
8+
#include "pybind11_tests.h"
9+
#include "pybind11/stl.h"
10+
11+
enum class Color {Red};
12+
13+
TEST_SUBMODULE(docstring_function_signature, m) {
14+
// test_docstring_function_signatures
15+
pybind11::enum_<Color> (m, "Color").value("Red", Color::Red);
16+
m.def("a", [](Color) {}, pybind11::arg("a") = Color::Red);
17+
m.def("b", [](int) {}, pybind11::arg("a") = 1);
18+
m.def("c", [](std::vector<int>) {}, pybind11::arg("a") = std::vector<int> {{1, 2, 3, 4}});
19+
m.def("d", [](UserType) {}, pybind11::arg("a") = UserType {});
20+
m.def("e", [](std::pair<UserType, int>) {}, pybind11::arg("a") = std::make_pair<UserType, int>(UserType(), 4));
21+
m.def("f", [](std::vector<Color>) {}, pybind11::arg("a") = std::vector<Color> {Color::Red});
22+
m.def("g", [](std::tuple<int, Color, double>) {}, pybind11::arg("a") = std::make_tuple(4, Color::Red, 1.9));
23+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: utf-8 -*-
2+
from pybind11_tests import docstring_function_signature as m
3+
import sys
4+
5+
6+
def test_docstring_function_signature():
7+
def syntactically_valid(sig):
8+
try:
9+
complete_fnsig = "def " + sig + ": pass"
10+
ast.parse(complete_fnsig)
11+
return True
12+
except SyntaxError:
13+
return False
14+
15+
pass
16+
17+
methods = ["a", "b", "c", "d", "e", "f", "g"]
18+
root_module = "pybind11_tests"
19+
module = "{}.{}".format(root_module, "docstring_function_signature")
20+
expected_signatures = [
21+
"a(a: {0}.Color = {0}.Color.Red) -> None".format(module),
22+
"b(a: int = 1) -> None",
23+
"c(a: List[int] = [1, 2, 3, 4]) -> None",
24+
"d(a: {}.UserType = ...) -> None".format(root_module),
25+
"e(a: Tuple[{}.UserType, int] = (..., 4)) -> None".format(root_module),
26+
"f(a: List[{0}.Color] = [{0}.Color.Red]) -> None".format(module),
27+
"g(a: Tuple[int, {0}.Color, float] = (4, {0}.Color.Red, 1.9)) -> None".format(
28+
module
29+
),
30+
]
31+
32+
for method, signature in zip(methods, expected_signatures):
33+
docstring = getattr(m, method).__doc__.strip("\n")
34+
assert docstring == signature
35+
36+
if sys.version_info.major >= 3 and sys.version_info.minor >= 5:
37+
import ast
38+
39+
for method in methods:
40+
docstring = getattr(m, method).__doc__.strip("\n")
41+
assert syntactically_valid(docstring)

0 commit comments

Comments
 (0)