Fix Python docstring for overloads with some Doxygen comments

The existing code didn't work correctly if the last element of the
overload set didn't have a Doxygen comment: in this case, no docstring
was generated at all.

Fix this by trying to find any overload with a Doxygen comment, as
Python docstring is common for all of them: add a helper function to do
it and use it for both all kinds of ordinary functions (global, member
and static) and __init__ functions generated for C++ ctors, as docstring
was generated in the same wrong way for all of them in different places.

Note that currently the overloads without Doxygen comments are not
documented at all at Python level, as saying "there exist other
overloads but there is no documentation for them" doesn't seem very
useful and there doesn't seem anything else that we could do.

Add a new unit test for testing all the different combinations of
overloaded functions with and without Doxygen comments.
This commit is contained in:
Vadim Zeitlin 2024-07-18 13:55:48 +02:00
parent eb5a599e1c
commit 60a695381e
4 changed files with 215 additions and 12 deletions

View File

@ -730,6 +730,7 @@ DOXYGEN_TEST_CASES += \
doxygen_ignore \
doxygen_misc_constructs \
doxygen_nested_class \
doxygen_overloads \
doxygen_parsing \
doxygen_parsing_enums \
doxygen_translate \

View File

@ -0,0 +1,95 @@
%module doxygen_overloads
%inline %{
void overloadWithNoDoc(int) { }
void overloadWithNoDoc(double) { }
/// Doc for first overload.
void overloadWithFirstDoc(int) { }
void overloadWithFirstDoc(double) { }
void overloadWithSecondDoc(int) { }
/// Doc for second overload.
void overloadWithSecondDoc(double) { }
/// Doc for both overloads, first.
void overloadWithBothDocs(int) { }
/// Doc for both overloads, second.
void overloadWithBothDocs(double) { }
/// Doc for some overloads, first.
void overloadWithSomeDocs(int) { }
void overloadWithSomeDocs(double) { }
/// Doc for some overloads, third.
void overloadWithSomeDocs(char) { }
/// Doc for some other overloads, first.
void overloadWithSomeOtherDocs(int) { }
/// Doc for some other overloads, second.
void overloadWithSomeOtherDocs(double) { }
void overloadWithSomeOtherDocs(char) { }
// Also test different kinds of member functions.
struct S {
/// Doc for first static overload.
static void staticOverloadWithFirstDoc(int) { }
static void staticOverloadWithFirstDoc(double) { }
/// Doc for first member overload.
void memberOverloadWithFirstDoc(int) { }
void memberOverloadWithFirstDoc(double) { }
};
// Class ctors are handled differently from the other functions, so check them too.
struct ClassWithNoDoc {
ClassWithNoDoc(int) { }
ClassWithNoDoc(double) { }
};
struct ClassWithFirstDoc {
/// Doc for first ctor.
ClassWithFirstDoc(int) { }
ClassWithFirstDoc(double) { }
};
struct ClassWithSecondDoc {
ClassWithSecondDoc(int) { }
/// Doc for second ctor.
ClassWithSecondDoc(double) { }
};
struct ClassWithBothDocs {
/// Doc for both ctors, first.
ClassWithBothDocs(int) { }
/// Doc for both ctors, second.
ClassWithBothDocs(double) { }
};
struct ClassWithSomeDocs {
/// Doc for some ctors, first.
ClassWithSomeDocs(int) { }
ClassWithSomeDocs(double) { }
/// Doc for some ctors, third.
ClassWithSomeDocs(char) { }
};
struct ClassWithSomeOtherDocs {
/// Doc for some other ctors, first.
ClassWithSomeOtherDocs(int) { }
/// Doc for some other ctors, second.
ClassWithSomeOtherDocs(double) { }
ClassWithSomeOtherDocs(char) { }
};
#ifdef SWIGPYTHON_BUILTIN
bool is_python_builtin() { return true; }
#else
bool is_python_builtin() { return false; }
#endif
%}

View File

@ -0,0 +1,83 @@
import doxygen_overloads
import inspect
import comment_verifier
if inspect.getdoc(doxygen_overloads.overloadWithNoDoc) is not None:
raise Exception("No docstring expected for overloadWithNoDoc.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.overloadWithFirstDoc),
"Doc for first overload.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.overloadWithSecondDoc),
"Doc for second overload.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.overloadWithBothDocs),
r"""*Overload 1:*
Doc for both overloads, first.
|
*Overload 2:*
Doc for both overloads, second.""")
comment_verifier.check(inspect.getdoc(doxygen_overloads.overloadWithSomeDocs),
r"""*Overload 1:*
Doc for some overloads, first.
|
*Overload 2:*
Doc for some overloads, third.""")
comment_verifier.check(inspect.getdoc(doxygen_overloads.overloadWithSomeOtherDocs),
r"""*Overload 1:*
Doc for some other overloads, first.
|
*Overload 2:*
Doc for some other overloads, second.""")
comment_verifier.check(inspect.getdoc(doxygen_overloads.S.staticOverloadWithFirstDoc),
"Doc for first static overload.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.S.memberOverloadWithFirstDoc),
"Doc for first member overload.")
# As mentioned in doxygen_parsing_runme.py, docstrings for __init__ can't be specified when using "-builtin", so skip these checks in this case.
if not doxygen_overloads.is_python_builtin():
# Do not check for ClassWithNoDoc ctor docstring, as Python provides a standard one
# ('Initialize self. See help(type(self)) for accurate signature.') if none is explicitly specified.
comment_verifier.check(inspect.getdoc(doxygen_overloads.ClassWithFirstDoc.__init__),
"Doc for first ctor.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.ClassWithSecondDoc.__init__),
"Doc for second ctor.")
comment_verifier.check(inspect.getdoc(doxygen_overloads.ClassWithBothDocs.__init__),
r"""*Overload 1:*
Doc for both ctors, first.
|
*Overload 2:*
Doc for both ctors, second.""")
comment_verifier.check(inspect.getdoc(doxygen_overloads.ClassWithSomeDocs.__init__),
r"""*Overload 1:*
Doc for some ctors, first.
|
*Overload 2:*
Doc for some ctors, third.""")
comment_verifier.check(inspect.getdoc(doxygen_overloads.ClassWithSomeOtherDocs.__init__),
r"""*Overload 1:*
Doc for some other ctors, first.
|
*Overload 2:*
Doc for some other ctors, second.""")

View File

@ -1490,6 +1490,26 @@ public:
);
}
/* ------------------------------------------------------------
* find_overload_with_docstring()
*
* This function should be called with the node pointing to the
* last element of an overload set and returns an overload with
* a docstring or null if there are none.
*
* The idea is that, because the Python docstring is shared by
* all overloads, it's this function return value and not the
* node itself which needs to be passes to docstring() later.
* ------------------------------------------------------------ */
Node* find_overload_with_docstring(Node* n) {
for (Node* node_with_doc = n; node_with_doc; node_with_doc = Getattr(node_with_doc, "sym:previousSibling")) {
if (have_docstring(node_with_doc))
return node_with_doc;
}
return NULL;
}
/* ------------------------------------------------------------
* build_combined_docstring()
*
@ -2432,8 +2452,12 @@ public:
if (!fast || olddefs) {
/* Make a wrapper function to insert the code into */
Printv(f_dest, "\n", "def ", name, "(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_dest, tab4, docstring(n, AUTODOC_FUNC, tab4, true), "\n", NIL);
// When handling the last overloaded function in an overload set (and we're only called for the last one if the function is overloaded at all), we need to
// output the docstring if any of the overloads has any documentation, not just this last one.
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_dest, tab4, docstring(node_with_doc, AUTODOC_FUNC, tab4, true), "\n", NIL);
if (have_pythonprepend(n))
Printv(f_dest, indent_pythoncode(pythonprepend(n), tab4, Getfile(n), Getline(n), "%pythonprepend or %feature(\"pythonprepend\")"), "\n", NIL);
if (have_pythonappend(n)) {
@ -4748,14 +4772,14 @@ public:
if (!have_addtofunc(n)) {
if (!fastproxy || olddefs) {
Printv(f_shadow, "\n", tab4, "def ", symname, "(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_shadow, tab8, docstring(n, AUTODOC_METHOD, tab8), "\n", NIL);
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_shadow, tab8, docstring(node_with_doc, AUTODOC_METHOD, tab8), "\n", NIL);
Printv(f_shadow, tab8, "return ", funcCall(fullname, callParms), "\n", NIL);
}
} else {
Printv(f_shadow, "\n", tab4, "def ", symname, "(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_shadow, tab8, docstring(n, AUTODOC_METHOD, tab8), "\n", NIL);
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_shadow, tab8, docstring(node_with_doc, AUTODOC_METHOD, tab8), "\n", NIL);
if (have_pythonprepend(n)) {
fproxy = 0;
Printv(f_shadow, indent_pythoncode(pythonprepend(n), tab8, Getfile(n), Getline(n), "%pythonprepend or %feature(\"pythonprepend\")"), "\n", NIL);
@ -4846,8 +4870,8 @@ public:
String *callParms = make_pyParmList(n, false, true, kw);
Printv(f_shadow, "\n", tab4, "@staticmethod", NIL);
Printv(f_shadow, "\n", tab4, "def ", symname, "(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_shadow, tab8, docstring(n, AUTODOC_STATICFUNC, tab8), "\n", NIL);
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_shadow, tab8, docstring(node_with_doc, AUTODOC_STATICFUNC, tab8), "\n", NIL);
if (have_pythonprepend(n))
Printv(f_shadow, indent_pythoncode(pythonprepend(n), tab8, Getfile(n), Getline(n), "%pythonprepend or %feature(\"pythonprepend\")"), "\n", NIL);
if (have_pythonappend(n)) {
@ -4953,8 +4977,8 @@ public:
}
Printv(f_shadow, "\n", tab4, "def __init__(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_shadow, tab8, docstring(n, AUTODOC_CTOR, tab8), "\n", NIL);
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_shadow, tab8, docstring(node_with_doc, AUTODOC_CTOR, tab8), "\n", NIL);
if (have_pythonprepend(n))
Printv(f_shadow, indent_pythoncode(pythonprepend(n), tab8, Getfile(n), Getline(n), "%pythonprepend or %feature(\"pythonprepend\")"), "\n", NIL);
Printv(f_shadow, pass_self, NIL);
@ -4981,8 +5005,8 @@ public:
String *callParms = make_pyParmList(n, false, true, allow_kwargs);
Printv(f_shadow_stubs, "\ndef ", symname, "(", parms, ")", returnTypeAnnotation(n), ":\n", NIL);
if (have_docstring(n))
Printv(f_shadow_stubs, tab4, docstring(n, AUTODOC_CTOR, tab4), "\n", NIL);
if (Node* node_with_doc = find_overload_with_docstring(n))
Printv(f_shadow_stubs, tab4, docstring(node_with_doc, AUTODOC_CTOR, tab4), "\n", NIL);
if (have_pythonprepend(n))
Printv(f_shadow_stubs, indent_pythoncode(pythonprepend(n), tab4, Getfile(n), Getline(n), "%pythonprepend or %feature(\"pythonprepend\")"), "\n", NIL);
Printv(f_shadow_stubs, tab4, "val = ", funcCall(subfunc, callParms), "\n", NIL);