From 9a110e6da84a7f23c3a447e4325b62db6f6e1f28 Mon Sep 17 00:00:00 2001 From: Alexander Stukowski Date: Tue, 15 Nov 2016 12:38:05 +0100 Subject: [PATCH] Provide more control over automatic generation of docstrings (#486) Added the docstring_options class, which gives global control over the generation of docstrings and function signatures. --- CMakeLists.txt | 1 + docs/advanced/misc.rst | 23 +++++++++++ include/pybind11/options.h | 65 ++++++++++++++++++++++++++++++++ include/pybind11/pybind11.h | 26 +++++++------ tests/CMakeLists.txt | 1 + tests/test_docstring_options.cpp | 53 ++++++++++++++++++++++++++ tests/test_docstring_options.py | 32 ++++++++++++++++ 7 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 include/pybind11/options.h create mode 100644 tests/test_docstring_options.cpp create mode 100644 tests/test_docstring_options.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 48d3cc7e8..6aa2beaeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ set(PYBIND11_HEADERS include/pybind11/common.h include/pybind11/complex.h include/pybind11/descr.h + include/pybind11/options.h include/pybind11/eigen.h include/pybind11/eval.h include/pybind11/functional.h diff --git a/docs/advanced/misc.rst b/docs/advanced/misc.rst index b0719065f..c13df7bf8 100644 --- a/docs/advanced/misc.rst +++ b/docs/advanced/misc.rst @@ -202,5 +202,28 @@ work, it is important that all lines are indented consistently, i.e.: ---------- )mydelimiter"); +By default, pybind11 automatically generates and prepends a signature to the docstring of a function +registered with ``module::def()`` and ``class_::def()``. Sometimes this +behavior is not desirable, because you want to provide your own signature or remove +the docstring completely to exclude the function from the Sphinx documentation. +The class ``options`` allows you to selectively suppress auto-generated signatures: + +.. code-block:: cpp + + PYBIND11_PLUGIN(example) { + py::module m("example", "pybind11 example plugin"); + + py::options options; + options.disable_function_signatures(); + + m.def("add", [](int a, int b) { return a + b; }, "A function which adds two numbers"); + + return m.ptr(); + } + +Note that changes to the settings affect only function bindings created during the +lifetime of the ``options`` instance. When it goes out of scope at the end of the module's init function, +the default settings are restored to prevent unwanted side effects. + .. [#f4] http://www.sphinx-doc.org .. [#f5] http://github.com/pybind/python_example diff --git a/include/pybind11/options.h b/include/pybind11/options.h new file mode 100644 index 000000000..3105551dd --- /dev/null +++ b/include/pybind11/options.h @@ -0,0 +1,65 @@ +/* + pybind11/options.h: global settings that are configurable at runtime. + + Copyright (c) 2016 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "common.h" + +NAMESPACE_BEGIN(pybind11) + +class options { +public: + + // Default RAII constructor, which leaves settings as they currently are. + options() : previous_state(global_state()) {} + + // Class is non-copyable. + options(const options&) = delete; + options& operator=(const options&) = delete; + + // Destructor, which restores settings that were in effect before. + ~options() { + global_state() = previous_state; + } + + // Setter methods (affect the global state): + + options& disable_user_defined_docstrings() & { global_state().show_user_defined_docstrings = false; return *this; } + + options& enable_user_defined_docstrings() & { global_state().show_user_defined_docstrings = true; return *this; } + + options& disable_function_signatures() & { global_state().show_function_signatures = false; return *this; } + + options& enable_function_signatures() & { global_state().show_function_signatures = true; return *this; } + + // Getter methods (return the global state): + + static bool show_user_defined_docstrings() { return global_state().show_user_defined_docstrings; } + + static bool show_function_signatures() { return global_state().show_function_signatures; } + + // This type is not meant to be allocated on the heap. + void* operator new(size_t) = delete; + +private: + + struct state { + bool show_user_defined_docstrings = true; //< Include user-supplied texts in docstrings. + bool show_function_signatures = true; //< Include auto-generated function signatures in docstrings. + }; + + static state &global_state() { + static state instance; + return instance; + } + + state previous_state; +}; + +NAMESPACE_END(pybind11) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 93f55ef96..4c5c830a9 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -34,6 +34,7 @@ #endif #include "attr.h" +#include "options.h" NAMESPACE_BEGIN(pybind11) @@ -306,7 +307,7 @@ protected: int index = 0; /* Create a nice pydoc rec including all signatures and docstrings of the functions in the overload chain */ - if (chain) { + if (chain && options::show_function_signatures()) { // First a generic signature signatures += rec->name; signatures += "(*args, **kwargs)\n"; @@ -314,15 +315,17 @@ protected: } // Then specific overload signatures for (auto it = chain_start; it != nullptr; it = it->next) { - if (chain) - signatures += std::to_string(++index) + ". "; - signatures += rec->name; - signatures += it->signature; - signatures += "\n"; - if (it->doc && strlen(it->doc) > 0) { + if (options::show_function_signatures()) { + if (chain) + signatures += std::to_string(++index) + ". "; + signatures += rec->name; + signatures += it->signature; signatures += "\n"; + } + if (it->doc && strlen(it->doc) > 0 && options::show_user_defined_docstrings()) { + if (options::show_function_signatures()) signatures += "\n"; signatures += it->doc; - signatures += "\n"; + if (options::show_function_signatures()) signatures += "\n"; } if (it->next) signatures += "\n"; @@ -532,6 +535,7 @@ public: PYBIND11_OBJECT_DEFAULT(module, object, PyModule_Check) explicit module(const char *name, const char *doc = nullptr) { + if (!options::show_user_defined_docstrings()) doc = nullptr; #if PY_MAJOR_VERSION >= 3 PyModuleDef *def = new PyModuleDef(); memset(def, 0, sizeof(PyModuleDef)); @@ -562,7 +566,7 @@ public: std::string full_name = std::string(PyModule_GetName(m_ptr)) + std::string(".") + std::string(name); module result(PyImport_AddModule(full_name.c_str()), true); - if (doc) + if (doc && options::show_user_defined_docstrings()) result.attr("__doc__") = pybind11::str(doc); attr(name) = result; return result; @@ -669,7 +673,7 @@ protected: : std::string(rec->name)); char *tp_doc = nullptr; - if (rec->doc) { + if (rec->doc && options::show_user_defined_docstrings()) { /* Allocate memory for docstring (using PyObject_MALLOC, since Python will free this later on) */ size_t size = strlen(rec->doc) + 1; @@ -1114,7 +1118,7 @@ public: rec_fset->doc = strdup(rec_fset->doc); } } - pybind11::str doc_obj = pybind11::str(rec_fget->doc ? rec_fget->doc : ""); + pybind11::str doc_obj = pybind11::str((rec_fget->doc && pybind11::options::show_user_defined_docstrings()) ? rec_fget->doc : ""); object property( PyObject_CallFunctionObjArgs((PyObject *) &PyProperty_Type, fget.ptr() ? fget.ptr() : Py_None, fset.ptr() ? fset.ptr() : Py_None, Py_None, doc_obj.ptr(), nullptr), false); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cbb87beb8..cb87fd812 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(PYBIND11_TEST_FILES test_class_args.cpp test_constants_and_functions.cpp test_copy_move_policies.cpp + test_docstring_options.cpp test_eigen.cpp test_enum.cpp test_eval.cpp diff --git a/tests/test_docstring_options.cpp b/tests/test_docstring_options.cpp new file mode 100644 index 000000000..74178c272 --- /dev/null +++ b/tests/test_docstring_options.cpp @@ -0,0 +1,53 @@ +/* + tests/test_docstring_options.cpp -- generation of docstrings and signatures + + Copyright (c) 2016 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "pybind11_tests.h" + +struct DocstringTestFoo { + int value; + void setValue(int v) { value = v; } + int getValue() const { return value; } +}; + +test_initializer docstring_generation([](py::module &m) { + + { + py::options options; + options.disable_function_signatures(); + + m.def("test_function1", [](int, int) {}, py::arg("a"), py::arg("b")); + m.def("test_function2", [](int, int) {}, py::arg("a"), py::arg("b"), "A custom docstring"); + + options.enable_function_signatures(); + + m.def("test_function3", [](int, int) {}, py::arg("a"), py::arg("b")); + m.def("test_function4", [](int, int) {}, py::arg("a"), py::arg("b"), "A custom docstring"); + + options.disable_function_signatures().disable_user_defined_docstrings(); + + m.def("test_function5", [](int, int) {}, py::arg("a"), py::arg("b"), "A custom docstring"); + + { + py::options nested_options; + nested_options.enable_user_defined_docstrings(); + m.def("test_function6", [](int, int) {}, py::arg("a"), py::arg("b"), "A custom docstring"); + } + } + + m.def("test_function7", [](int, int) {}, py::arg("a"), py::arg("b"), "A custom docstring"); + + { + py::options options; + options.disable_user_defined_docstrings(); + + py::class_(m, "DocstringTestFoo", "This is a class docstring") + .def_property("value_prop", &DocstringTestFoo::getValue, &DocstringTestFoo::setValue, "This is a property docstring") + ; + } +}); diff --git a/tests/test_docstring_options.py b/tests/test_docstring_options.py new file mode 100644 index 000000000..9cd499331 --- /dev/null +++ b/tests/test_docstring_options.py @@ -0,0 +1,32 @@ +from pybind11_tests import ConstructorStats + +def test_docstring_options(capture): + from pybind11_tests import (test_function1, test_function2, test_function3, + test_function4, test_function5, test_function6, + test_function7, DocstringTestFoo) + + # options.disable_function_signatures() + assert not test_function1.__doc__ + + assert test_function2.__doc__ == "A custom docstring" + + # options.enable_function_signatures() + assert test_function3.__doc__ .startswith("test_function3(a: int, b: int) -> None") + + assert test_function4.__doc__ .startswith("test_function4(a: int, b: int) -> None") + assert test_function4.__doc__ .endswith("A custom docstring\n") + + # options.disable_function_signatures() + # options.disable_user_defined_docstrings() + assert not test_function5.__doc__ + + # nested options.enable_user_defined_docstrings() + assert test_function6.__doc__ == "A custom docstring" + + # RAII destructor + assert test_function7.__doc__ .startswith("test_function7(a: int, b: int) -> None") + assert test_function7.__doc__ .endswith("A custom docstring\n") + + # Suppression of user-defined docstrings for non-function objects + assert not DocstringTestFoo.__doc__ + assert not DocstringTestFoo.value_prop.__doc__