Skip to content

Commit 59cf701

Browse files
detail: Copydoc from multiple sources
1 parent 5c11309 commit 59cf701

1 file changed

Lines changed: 59 additions & 10 deletions

File tree

src/libsemigroups_pybind11/detail/decorators.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,61 @@
1111
libsemigroups_pybind11.
1212
"""
1313

14+
import re as _re
1415

15-
def copydoc(original, *args):
16+
17+
def _get_overloaded_doc(func):
18+
"""Return the docstring of a function as if that function was overloaded"""
19+
doc = func.__doc__
20+
if "Overloaded function." in doc:
21+
return doc
22+
return "1. " + doc
23+
24+
25+
def _correct_overloads(target, base_func, extra_funcs):
26+
"""
27+
Fix the docstring of copied overloaded functions
28+
29+
When functions are bound, pybind11 adds some decorations to the docstring
30+
(unless this is explicitly turned off using
31+
py::options::disable_function_signatures()). When a function is overloaded,
32+
this decoration includes the string 'Overloaded function.', a new signature
33+
with parameters *args and **kwargs, and some enumeration of the different
34+
overloads.
35+
36+
To combine multiple docstrings of functions, it is necessary to:
37+
* remove all but the first occurrence of the pybind11 inserted strings;
38+
* ensure every function is documented as if it was overloaded; and
39+
* number the overloads 1, 2, ..., N.
40+
41+
This function does these things.
42+
"""
43+
target_name = target.__name__
44+
old_names = set(fn.__name__ for fn in extra_funcs) | {base_func.__name__}
45+
base_doc = _get_overloaded_doc(base_func)
46+
extra_doc = "".join([_get_overloaded_doc(func) for func in extra_funcs])
47+
48+
# Remove pybind11 inserted strings
49+
replacements = {
50+
f"{old_name}(*args, **kwargs)\n": "" for old_name in old_names
51+
}
52+
replacements["Overloaded function.\n"] = ""
53+
for old, new in replacements.items():
54+
extra_doc = extra_doc.replace(old, new)
55+
56+
# Fix overload numbering
57+
overload_counter = 1
58+
doc_blocks = _re.split(
59+
rf"\d+\. (?:{'|'.join(old_names)})", base_doc + extra_doc
60+
)
61+
new_doc = doc_blocks[0]
62+
for doc_block in doc_blocks[1:]:
63+
new_doc += f"{overload_counter}. {target_name}" + doc_block
64+
overload_counter += 1
65+
return new_doc
66+
67+
68+
def copydoc(func, *extra_funcs):
1669
"""
1770
Decorator that can be used to copy the doc from one function to another,
1871
for example:
@@ -21,17 +74,13 @@ def copydoc(original, *args):
2174
def __init___(self) -> None:
2275
pass
2376
"""
24-
25-
original_doc = original.__doc__
26-
patterns = {"Overloaded function.": "", "__init__(*args, **kwargs)": ""}
27-
for arg in args:
28-
doc = arg.__doc__
29-
for old, new in patterns.items():
30-
doc = doc.replace(old, new)
31-
original_doc += doc
77+
new_doc = func.__doc__
3278

3379
def wrapper(target):
34-
target.__doc__ = original_doc
80+
if extra_funcs:
81+
target.__doc__ = _correct_overloads(target, func, extra_funcs)
82+
else:
83+
target.__doc__ = new_doc
3584
return target
3685

3786
return wrapper

0 commit comments

Comments
 (0)