Skip to content

Commit b9359ce

Browse files
Remove newlines from docstring signature (#4735)
* Remove newlines from docstring signature * Jean/dev (#1) Replace newlines in arg values with spaces * style: pre-commit fixes * Don't use std::find_if for C++ 11 compatibility * Avoid implicit char to bool conversion * Test default arguments for line breaks * style: pre-commit fixes * Separate Eigen tests * style: pre-commit fixes * Fix merge * Try importing numpy * Avoid unreferenced variable in catch block * style: pre-commit fixes * Update squash function * Reduce try block * Additional test cases * style: pre-commit fixes * Put statement inside braces * Move string into function body * Rename repr for better readability. Make constr explicit. * Add multiline string default argument test case * style: pre-commit fixes * Add std namespace, do not modify string repr * Test for all space chars, test str repr not modified --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f47ff32 commit b9359ce

File tree

5 files changed

+138
-1
lines changed

5 files changed

+138
-1
lines changed

include/pybind11/pybind11.h

+40-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,45 @@ PYBIND11_WARNING_DISABLE_MSVC(4127)
5252

5353
PYBIND11_NAMESPACE_BEGIN(detail)
5454

55+
inline std::string replace_newlines_and_squash(const char *text) {
56+
const char *whitespaces = " \t\n\r\f\v";
57+
std::string result(text);
58+
bool previous_is_whitespace = false;
59+
60+
// Do not modify string representations
61+
char first_char = result[0];
62+
char last_char = result[result.size() - 1];
63+
if (first_char == last_char && first_char == '\'') {
64+
return result;
65+
}
66+
result.clear();
67+
68+
// Replace characters in whitespaces array with spaces and squash consecutive spaces
69+
while (*text != '\0') {
70+
if (std::strchr(whitespaces, *text)) {
71+
if (!previous_is_whitespace) {
72+
result += ' ';
73+
previous_is_whitespace = true;
74+
}
75+
} else {
76+
result += *text;
77+
previous_is_whitespace = false;
78+
}
79+
++text;
80+
}
81+
82+
// Strip leading and trailing whitespaces
83+
const size_t str_begin = result.find_first_not_of(whitespaces);
84+
if (str_begin == std::string::npos) {
85+
return "";
86+
}
87+
88+
const size_t str_end = result.find_last_not_of(whitespaces);
89+
const size_t str_range = str_end - str_begin + 1;
90+
91+
return result.substr(str_begin, str_range);
92+
}
93+
5594
// Apply all the extensions translators from a list
5695
// Return true if one of the translators completed without raising an exception
5796
// itself. Return of false indicates that if there are other translators
@@ -424,7 +463,7 @@ class cpp_function : public function {
424463
// Write default value if available.
425464
if (!is_starred && arg_index < rec->args.size() && rec->args[arg_index].descr) {
426465
signature += " = ";
427-
signature += rec->args[arg_index].descr;
466+
signature += detail::replace_newlines_and_squash(rec->args[arg_index].descr);
428467
}
429468
// Separator for positional-only arguments (placed after the
430469
// argument, rather than before like *

tests/test_eigen_matrix.cpp

+17
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,23 @@ TEST_SUBMODULE(eigen_matrix, m) {
330330
m.def("dense_c", [mat]() -> DenseMatrixC { return DenseMatrixC(mat); });
331331
m.def("dense_copy_r", [](const DenseMatrixR &m) -> DenseMatrixR { return m; });
332332
m.def("dense_copy_c", [](const DenseMatrixC &m) -> DenseMatrixC { return m; });
333+
// test_defaults
334+
bool have_numpy = true;
335+
try {
336+
py::module_::import("numpy");
337+
} catch (const py::error_already_set &) {
338+
have_numpy = false;
339+
}
340+
if (have_numpy) {
341+
py::module_::import("numpy");
342+
Eigen::Matrix<double, 3, 3> defaultMatrix = Eigen::Matrix3d::Identity();
343+
m.def(
344+
"defaults_mat", [](const Eigen::Matrix3d &) {}, py::arg("mat") = defaultMatrix);
345+
346+
Eigen::VectorXd defaultVector = Eigen::VectorXd::Ones(32);
347+
m.def(
348+
"defaults_vec", [](const Eigen::VectorXd &) {}, py::arg("vec") = defaultMatrix);
349+
}
333350
// test_sparse, test_sparse_signature
334351
m.def("sparse_r", [mat]() -> SparseMatrixR {
335352
// NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn)

tests/test_eigen_matrix.py

+5
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,11 @@ def test_dense_signature(doc):
716716
)
717717

718718

719+
def test_defaults(doc):
720+
assert "\n" not in str(doc(m.defaults_mat))
721+
assert "\n" not in str(doc(m.defaults_vec))
722+
723+
719724
def test_named_arguments():
720725
a = np.array([[1.0, 2], [3, 4], [5, 6]])
721726
b = np.ones((2, 1))

tests/test_kwargs_and_defaults.cpp

+44
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,50 @@ TEST_SUBMODULE(kwargs_and_defaults, m) {
4242
m.def("kw_func_udl", kw_func, "x"_a, "y"_a = 300);
4343
m.def("kw_func_udl_z", kw_func, "x"_a, "y"_a = 0);
4444

45+
// test line breaks in default argument representation
46+
struct CustomRepr {
47+
std::string repr_string;
48+
49+
explicit CustomRepr(const std::string &repr) : repr_string(repr) {}
50+
51+
std::string __repr__() const { return repr_string; }
52+
};
53+
54+
py::class_<CustomRepr>(m, "CustomRepr")
55+
.def(py::init<const std::string &>())
56+
.def("__repr__", &CustomRepr::__repr__);
57+
58+
m.def(
59+
"kw_lb_func0",
60+
[](const CustomRepr &) {},
61+
py::arg("custom") = CustomRepr(" array([[A, B], [C, D]]) "));
62+
m.def(
63+
"kw_lb_func1",
64+
[](const CustomRepr &) {},
65+
py::arg("custom") = CustomRepr(" array([[A, B],\n[C, D]]) "));
66+
m.def(
67+
"kw_lb_func2",
68+
[](const CustomRepr &) {},
69+
py::arg("custom") = CustomRepr("\v\n array([[A, B], [C, D]])"));
70+
m.def(
71+
"kw_lb_func3",
72+
[](const CustomRepr &) {},
73+
py::arg("custom") = CustomRepr("array([[A, B], [C, D]]) \f\n"));
74+
m.def(
75+
"kw_lb_func4",
76+
[](const CustomRepr &) {},
77+
py::arg("custom") = CustomRepr("array([[A, B],\n\f\n[C, D]])"));
78+
m.def(
79+
"kw_lb_func5",
80+
[](const CustomRepr &) {},
81+
py::arg("custom") = CustomRepr("array([[A, B],\r [C, D]])"));
82+
m.def(
83+
"kw_lb_func6", [](const CustomRepr &) {}, py::arg("custom") = CustomRepr(" \v\t "));
84+
m.def(
85+
"kw_lb_func7",
86+
[](const std::string &) {},
87+
py::arg("str_arg") = "First line.\n Second line.");
88+
4589
// test_args_and_kwargs
4690
m.def("args_function", [](py::args args) -> py::tuple {
4791
PYBIND11_WARNING_PUSH

tests/test_kwargs_and_defaults.py

+32
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ def test_function_signatures(doc):
2323
doc(m.KWClass.foo1)
2424
== "foo1(self: m.kwargs_and_defaults.KWClass, x: int, y: float) -> None"
2525
)
26+
assert (
27+
doc(m.kw_lb_func0)
28+
== "kw_lb_func0(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
29+
)
30+
assert (
31+
doc(m.kw_lb_func1)
32+
== "kw_lb_func1(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
33+
)
34+
assert (
35+
doc(m.kw_lb_func2)
36+
== "kw_lb_func2(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
37+
)
38+
assert (
39+
doc(m.kw_lb_func3)
40+
== "kw_lb_func3(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
41+
)
42+
assert (
43+
doc(m.kw_lb_func4)
44+
== "kw_lb_func4(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
45+
)
46+
assert (
47+
doc(m.kw_lb_func5)
48+
== "kw_lb_func5(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None"
49+
)
50+
assert (
51+
doc(m.kw_lb_func6)
52+
== "kw_lb_func6(custom: m.kwargs_and_defaults.CustomRepr = ) -> None"
53+
)
54+
assert (
55+
doc(m.kw_lb_func7)
56+
== "kw_lb_func7(str_arg: str = 'First line.\\n Second line.') -> None"
57+
)
2658

2759

2860
def test_named_arguments():

0 commit comments

Comments
 (0)