From 272faa3d9c01131faccc917d18b7c67d308055c3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 27 Aug 2019 23:32:34 +0200 Subject: [PATCH] documentation/python: support documenting particular function overloads. --- doc/plugins/sphinx.rst | 24 ++- documentation/python.py | 126 ++++++++------ documentation/test_python/CMakeLists.txt | 2 +- .../pybind_external_overload_docs/docs.rst | 38 +++++ .../pybind_external_overload_docs.Class.html | 84 ++++++++++ .../pybind_external_overload_docs.cpp | 31 ++++ .../pybind_external_overload_docs.html | 158 ++++++++++++++++++ documentation/test_python/test_pybind.py | 10 ++ 8 files changed, 416 insertions(+), 57 deletions(-) create mode 100644 documentation/test_python/pybind_external_overload_docs/docs.rst create mode 100644 documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.Class.html create mode 100644 documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.cpp create mode 100644 documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.html diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst index abc6c373..63c9384b 100644 --- a/doc/plugins/sphinx.rst +++ b/doc/plugins/sphinx.rst @@ -248,7 +248,6 @@ doing the following will ensure it can be easily used: PYMEM_DOMAIN_OBJ c:var 1 c-api/memory.html#c.$ - ... - `Module, class, enum, function, property and data docs`_ ======================================================== @@ -305,6 +304,7 @@ function signature will cause a warning. Example: .. code:: rst .. py:function:: mymodule.MyContainer.add + :summary: Add a key/value pair to the container :param key: Key to add :param value: Corresponding value :param overwrite_existing: Overwrite existing value if already present @@ -312,5 +312,23 @@ function signature will cause a warning. Example: :return: The inserted tuple or the existing key/value pair in case ``overwrite_existing`` is not set - Add a key/value pair to the container, optionally overwriting the - previous value. + The operation has a :math:`\mathcal{O}(\log{}n)` complexity. + +For overloaded functions (such as those coming from pybind11), it's possible to +specify the full signature to distinguish between particular overloads. +Directives with the full signature have a priority, if no signature matches +given function, a signature-less directive is searched for as a fallback. +Example: + +.. code:: rst + + .. py:function:: magnum.math.dot(a: magnum.Complex, b: magnum.Complex) + :summary: Dot product of two complex numbers + + .. py:function:: magnum.math.dot(a: magnum.Quaternion, b: magnum.Quaternion) + :summary: Dot product of two quaternions + + .. py:function:: magnum.math.dot + :summary: Dot product + + .. this documentation will be used for all other overloads diff --git a/documentation/python.py b/documentation/python.py index 22d802e1..42d7f15f 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -797,19 +797,28 @@ def split_summary_content(doc: str) -> str: content = content.strip() return summary, ('\n'.join(['

{}

'.format(p) for p in content.split('\n\n')]) if content else None) -def extract_summary(state: State, external_docs, path: List[str], doc: str) -> str: +def extract_summary(state: State, external_docs, path: List[str], doc: str, *, signature=None) -> str: # Prefer external docs, if available path_str = '.'.join(path) - if path_str in external_docs and external_docs[path_str]['summary']: - return render_inline_rst(state, external_docs[path_str]['summary']) + if signature and path_str + signature in external_docs: + external_doc_entry = external_docs[path_str + signature] + elif path_str in external_docs: + external_doc_entry = external_docs[path_str] + else: + external_doc_entry = None + if external_doc_entry and external_doc_entry['summary']: + return render_inline_rst(state, external_doc_entry['summary']) # some modules (xml.etree) have None as a docstring :( # TODO: render as rst (config option for that) return html.escape(inspect.cleandoc(doc or '').partition('\n\n')[0]) -def extract_docs(state: State, external_docs, path: List[str], doc: str) -> Tuple[str, str]: +def extract_docs(state: State, external_docs, path: List[str], doc: str, *, signature=None) -> Tuple[str, str]: path_str = '.'.join(path) - if path_str in external_docs: + # If function signature is supplied, try that first + if signature and path_str + signature in external_docs: + external_doc_entry = external_docs[path_str + signature] + elif path_str in external_docs: external_doc_entry = external_docs[path_str] else: external_doc_entry = None @@ -1065,6 +1074,8 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: if not state.config['SEARCH_DISABLED']: page_url = state.name_map['.'.join(entry.path[:-1])].url + overloads = [] + # Extract the signature from the docstring for pybind11, since it can't # expose it to the metadata: https://github.com/pybind/pybind11/issues/990 # What's not solvable with metadata, however, are function overloads --- @@ -1084,8 +1095,7 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: out.name = entry.path[-1] out.params = [] out.has_complex_params = False - out.summary, out.content = extract_docs(state, state.function_docs, entry.path, summary) - out.has_details = bool(out.content) + out.has_details = False out.type, out.type_link = type, type_link # There's no other way to check staticmethods than to check for @@ -1130,6 +1140,7 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: break arg_types = [] + signature = [] for i, arg in enumerate(args): name, type, type_link, default = arg param = Empty() @@ -1138,9 +1149,11 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: if i == 0 and name == 'self': param.type, param.type_link = None, None arg_types += [None] + signature += ['self'] else: param.type, param.type_link = type, type_link arg_types += [type] + signature += ['{}: {}'.format(name, type)] if default: # If the type is a registered enum, try to make a link to # the value -- for an enum of type `module.EnumType`, @@ -1171,26 +1184,13 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: # thus name alone is not enough. out.id = state.config['ID_FORMATTER'](EntryType.OVERLOADED_FUNCTION, entry.path[-1:] + arg_types) - if not state.config['SEARCH_DISABLED']: - result = Empty() - result.flags = ResultFlag.from_type(ResultFlag.NONE, EntryType.FUNCTION) - result.url = '{}#{}'.format(page_url, out.id) - result.prefix = entry.path[:-1] - result.name = entry.path[-1] - result.params = [] - # If there's more than one overload, add the params as well to - # disambiguate - if len(funcs) != 1: - for i in range(len(out.params)): - param = out.params[i] - result.params += ['{}: {}'.format(param.name, make_relative_name(state, entry.path, arg_types[i])) if arg_types[i] else param.name] - state.search += [result] + # Get summary and details. Passing the signature as well, so + # different overloads can (but don't need to) have different docs. + out.summary, out.content = extract_docs(state, state.function_docs, entry.path, summary, signature='({})'.format(', '.join(signature))) + if out.content: out.has_details = True overloads += [out] - # TODO: assign docs and particular param docs to overloads - return overloads - # Sane introspection path for non-pybind11 code else: out = Empty() @@ -1236,43 +1236,57 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: param.kind = str(i.kind) out.params += [param] - # Get docs for each param and for the return value - path_str = '.'.join(entry.path) - if path_str in state.function_docs: - # Having no parameters documented is okay, having self - # undocumented as well. But having the rest documented only - # partially isn't okay. - if state.function_docs[path_str]['params']: - param_docs = state.function_docs[path_str]['params'] - used_params = set() - for param in out.params: - if param.name not in param_docs: - if param.name != 'self': - logging.warning("%s() parameter %s is not documented", path_str, param.name) - continue - param.content = render_inline_rst(state, param_docs[param.name]) - used_params.add(param.name) - out.has_param_details = True - out.has_details = True - # Having unused param docs isn't okay either - for name, _ in param_docs.items(): - if name not in used_params: - logging.warning("%s() documents parameter %s, which isn't in the signature", path_str, name) - - if state.function_docs[path_str]['return']: - out.return_value = render_inline_rst(state, state.function_docs[path_str]['return']) - out.has_details = True - # In CPython, some builtin functions (such as math.log) do not provide # metadata about their arguments. Source: # https://docs.python.org/3/library/inspect.html#inspect.signature except ValueError: param = Empty() param.name = '...' - param.name_type = param.name + param.type, param.type_link = None, None out.params = [param] out.type, out.type_link = None, None + overloads = [out] + + # Common path for parameter / return value docs and search + path_str = '.'.join(entry.path) + for out in overloads: + signature = '({})'.format(', '.join(['{}: {}'.format(param.name, param.type) if param.type else param.name for param in out.params])) + + # Get docs for each param and for the return value. Try this + # particular overload first, if not found then fall back to generic + # docs for all overloads. + if path_str + signature in state.function_docs: + function_docs = state.function_docs[path_str + signature] + elif path_str in state.function_docs: + function_docs = state.function_docs[path_str] + else: + function_docs = None + if function_docs: + # Having no parameters documented is okay, having self + # undocumented as well. But having the rest documented only + # partially isn't okay. + if function_docs['params']: + param_docs = function_docs['params'] + used_params = set() + for param in out.params: + if param.name not in param_docs: + if param.name != 'self': + logging.warning("%s%s parameter %s is not documented", path_str, signature, param.name) + continue + param.content = render_inline_rst(state, param_docs[param.name]) + used_params.add(param.name) + out.has_param_details = True + out.has_details = True + # Having unused param docs isn't okay either + for name, _ in param_docs.items(): + if name not in used_params: + logging.warning("%s%s documents parameter %s, which isn't in the signature", path_str, signature, name) + + if function_docs['return']: + out.return_value = render_inline_rst(state, function_docs['return']) + out.has_details = True + if not state.config['SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.from_type(ResultFlag.NONE, EntryType.FUNCTION) @@ -1280,9 +1294,15 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: result.prefix = entry.path[:-1] result.name = entry.path[-1] result.params = [] + # If the function has multiple overloads (e.g. pybind functions), + # add arguments to each to distinguish between them + if len(overloads) != 1: + for i in range(len(out.params)): + param = out.params[i] + result.params += ['{}: {}'.format(param.name, make_relative_name(state, entry.path, param.type)) if param.type else param.name] state.search += [result] - return [out] + return overloads def extract_property_doc(state: State, parent, entry: Empty): assert inspect.isdatadescriptor(entry.object) diff --git a/documentation/test_python/CMakeLists.txt b/documentation/test_python/CMakeLists.txt index 298c40f8..98424d47 100644 --- a/documentation/test_python/CMakeLists.txt +++ b/documentation/test_python/CMakeLists.txt @@ -27,7 +27,7 @@ project(McssDocumentationPybindTests) find_package(pybind11 CONFIG REQUIRED) -foreach(target pybind_signatures pybind_enums pybind_submodules pybind_type_links search_long_suffix_length) +foreach(target pybind_signatures pybind_enums pybind_external_overload_docs pybind_submodules pybind_type_links search_long_suffix_length) pybind11_add_module(${target} ${target}/${target}.cpp) set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${target}) endforeach() diff --git a/documentation/test_python/pybind_external_overload_docs/docs.rst b/documentation/test_python/pybind_external_overload_docs/docs.rst new file mode 100644 index 00000000..242c1c22 --- /dev/null +++ b/documentation/test_python/pybind_external_overload_docs/docs.rst @@ -0,0 +1,38 @@ +.. py:function:: pybind_external_overload_docs.foo(a: int, b: typing.Tuple[int, str]) + :param a: First parameter + :param b: Second parameter + + Details for the first overload. + +.. py:function:: pybind_external_overload_docs.foo(arg0: typing.Callable[[float, typing.List[float]], int]) + :param arg0: The caller + + Complex signatures in the second overload should be matched properly, too. + +.. py:function:: pybind_external_overload_docs.foo + :param name: Ha! + + This is a generic documentation and will be caught only by the third + overload. Luckily we just document that exact parameter. + +.. py:function:: pybind_external_overload_docs.foo(param: int) + :param param: This has a default value of 4 but that shouldn't be part of + the signature. + + Fourth overload has a default value. + +.. py:function:: pybind_external_overload_docs.Class.foo(self, index: int) + :return: Nothing at all. + + Class methods don't have type annotation on self. + +.. py:function:: pybind_external_overload_docs.Class.foo + + And the fallback matching works there, too. + +.. py:function:: pybind_external_overload_docs.foo(first: int) + :param second: But the second argument doesn't exist?! + +.. py:function:: pybind_external_overload_docs.foo(terrible: wow) + + This docs can't match any overload and thus get unused. diff --git a/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.Class.html b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.Class.html new file mode 100644 index 00000000..548939ea --- /dev/null +++ b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.Class.html @@ -0,0 +1,84 @@ + + + + + pybind_external_overload_docs.Class | My Python Project + + + + + +
+
+
+
+
+

+ pybind_external_overload_docs.Class class +

+

My fun class!

+
+

Contents

+ +
+
+

Methods

+
+
+ def foo(self, + index: int) -> None +
+
First overload
+
+ def foo(self, + name: str) -> None +
+
Second overload
+
+
+
+

Method documentation

+
+

+ def pybind_external_overload_docs.Class.foo(self, + index: int) -> None +

+

First overload

+ + + + + + + +
ReturnsNothing at all.
+

Class methods don't have type annotation on self.

+
+
+

+ def pybind_external_overload_docs.Class.foo(self, + name: str) -> None +

+

Second overload

+

And the fallback matching works there, too.

+
+
+
+
+
+
+ + diff --git a/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.cpp b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.cpp new file mode 100644 index 00000000..e5e8260a --- /dev/null +++ b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.cpp @@ -0,0 +1,31 @@ +#include +#include +#include /* needed for std::vector! */ +#include /* for std::function */ + +namespace py = pybind11; + +void foo1(int, const std::tuple&) {} +void foo2(std::function&)>) {} +void foo3(std::string) {} +void foo4(int) {} + +struct Class { + void foo1(int) {} + void foo2(std::string) {} +}; + +PYBIND11_MODULE(pybind_external_overload_docs, m) { + m.doc() = "pybind11 external overload docs"; + + m + .def("foo", &foo1, "First overload", py::arg("a"), py::arg("b")) + .def("foo", &foo2, "Second overload") + .def("foo", &foo3, "Third overload", py::arg("name")) + .def("foo", &foo4, "Fourth overload", py::arg("param") = 4) + .def("foo", &foo4, "This will produce param documentation mismatch warnings", py::arg("first")); + + py::class_(m, "Class", "My fun class!") + .def("foo", &Class::foo1, "First overload", py::arg("index")) + .def("foo", &Class::foo2, "Second overload", py::arg("name")); +} diff --git a/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.html b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.html new file mode 100644 index 00000000..2768ba36 --- /dev/null +++ b/documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.html @@ -0,0 +1,158 @@ + + + + + pybind_external_overload_docs | My Python Project + + + + + +
+
+
+
+
+

+ pybind_external_overload_docs module +

+

pybind11 external overload docs

+
+

Contents

+ +
+
+

Classes

+
+
class Class
+
My fun class!
+
+
+
+

Functions

+
+
+ def foo(a: int, + b: typing.Tuple[int, str]) -> None +
+
First overload
+
+ def foo(arg0: typing.Callable[[float, typing.List[float]], int], /) -> None +
+
Second overload
+
+ def foo(name: str) -> None +
+
Third overload
+
+ def foo(param: int = 4) -> None +
+
Fourth overload
+
+ def foo(first: int) -> None +
+
This will produce param documentation mismatch warnings
+
+
+
+

Function documentation

+
+

+ def pybind_external_overload_docs.foo(a: int, + b: typing.Tuple[int, str]) -> None +

+

First overload

+ + + + + + + + + + + + + + +
Parameters
aFirst parameter
bSecond parameter
+

Details for the first overload.

+
+
+

+ def pybind_external_overload_docs.foo(arg0: typing.Callable[[float, typing.List[float]], int], /) -> None +

+

Second overload

+ + + + + + + + + + +
Parameters
arg0The caller
+

Complex signatures in the second overload should be matched properly, too.

+
+
+

+ def pybind_external_overload_docs.foo(name: str) -> None +

+

Third overload

+ + + + + + + + + + +
Parameters
nameHa!
+

This is a generic documentation and will be caught only by the third +overload. Luckily we just document that exact parameter.

+
+
+

+ def pybind_external_overload_docs.foo(param: int = 4) -> None +

+

Fourth overload

+ + + + + + + + + + +
Parameters
paramThis has a default value of 4 but that shouldn't be part of +the signature.
+

Fourth overload has a default value.

+
+
+
+
+
+
+ + diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 6ce25359..b0aa1110 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -276,3 +276,13 @@ class TypeLinks(BaseInspectTestCase): }) self.assertEqual(*self.actual_expected_contents('pybind_type_links.html')) self.assertEqual(*self.actual_expected_contents('pybind_type_links.Foo.html')) + +class ExternalOverloadDocs(BaseInspectTestCase): + def test(self): + self.run_python({ + 'PLUGINS': ['m.sphinx'], + 'INPUT_DOCS': ['docs.rst'], + 'PYBIND11_COMPATIBILITY': True + }) + self.assertEqual(*self.actual_expected_contents('pybind_external_overload_docs.html')) + self.assertEqual(*self.actual_expected_contents('pybind_external_overload_docs.Class.html')) -- 2.30.2