'''
This module provides decorators and a superclass that allow methods to
"inherit" documentation from other given methods or similar methods from
super classes when a documentation is generated via Shinx.
Usage
#####
If documentation shall be 'inherited' from super classes, the base class must
inherit from the meta-class :py:class:`DocMetaSuperclass`.
Otherwise, the source of the documentation
must be specified via the :py:meth:`inherit_doc` decorator.
* Documentation of single arguments (numpy format) is taken from the super
method if not provided for the child.
* You may add a new description of the method and renew the documentation of
arguments to your liking, but undocumented arguments will be documented from the super class.
* Documentation can be replaced, inserted, added, or ignored.
* The particular procedure can be controlled by adding a marker string to the
beginning of the header, footer, type, or argument description.
* Description starting with ``#`` will be overwritten by the super method.
* Description starting with ``<!`` will be put before the description from the super method.
* Description starting with ``!>`` will be put behind the description from the super method.
* Description without starting marker will replace the description from the super method.
* Super methods can have documentation that is not carried over to the children.
* Lines after a line starting with ``~+~`` will be ignored by inheriting functions.
* The tool is applicable to both entire classes (via metaclasses) and single
methods (via decorators). The two can be combined.
* Via the decorator, multiple methods can be screened for suitable parameter definitions.
* This is useful if the methods of concern bundles many other methods.
Example 1: Inheriting documentation from the super class
********************************************************
.. code-block:: python
from vemomoto_core.tools.doc_utils import DocMetaSuperclass, inherit_doc
class BaseClass(metaclass=DocMetaSuperclass)
def mymethod(myargument):
"""This does something
~+~
This text will not be seen by the inheriting classes
Parameters
----------
myargument : int
Description of the argument
"""
[...]
@inherit_doc(mymethod)
def mymethod2(myargument, otherArgument):
""">!This description is added to the description of mymethod
(ignoring the section below ``~+~``)
Parameters
----------
otherArgument : int
Description of the other argument
[here the description of ``myargument`` will be inserted from mymethod]
"""
BaseClass.mymethod(myargument)
[...]
class MyClass1(BaseClass):
def mymethod2(myargument):
"""This overwirtes the description of ``BaseClass.mymethod``
[here the description of ``myargument`` from BaseClass.mymethod2 is inserted
(which in turn comes from BaseClass.mymethod); otherArgument is ignored]
"""
BaseClass.mymethod(myargument)
[...]
class MyClass2(BaseClass):
def mymethod2(myargument, otherArgument):
"""#This description will be overwritten
Parameters
----------
myargument : string <- this changes the type description only
otherArgument [here the type description from BaseClass will be inserted]
<! This text will be put before the argument description from BaseClass
"""
BaseClass.mymethod2(myargument, otherArgument)
[...]
Example 2: Inheriting documentation from an other method
********************************************************
.. code-block:: python
from vemomoto_core.tools.doc_utils import inherit_doc
def method1(arg1):
"""This does something
Parameters
----------
arg1 : type
Description
"""
[...]
def method2(arg2):
"""This does something
Parameters
----------
arg2 : type
Description
"""
[...]
def method3(arg3):
"""This does something
Parameters
----------
arg3 : type
Description
"""
[...]
@inherit_doc(method1, method2, method3)
def bundle_method(arg1, arg2, arg3):
"""This does something
[here the parameter descriptions from the other
methods will be inserted]
"""
[...]
'''
import inspect
import re
IGNORE_STR = "#"
PRIVATE_STR = "~+~"
INSERT_STR = "<!"
APPEND_STR = ">!"
[docs]def should_ignore(string):
return not string or not string.strip() or string.lstrip().startswith(IGNORE_STR)
[docs]def should_insert(string):
return string.lstrip().startswith(INSERT_STR)
[docs]def should_append(string):
return string.lstrip().startswith(APPEND_STR)
[docs]def strip_lines(string):
lines = string.splitlines(True)
for dirct in 0, -1:
while lines:
if not lines[dirct].strip():
del lines[dirct]
else:
break
return "".join(lines)
[docs]def inherit_doc(*fromfuncs):
"""
Decorator: Copy elements from the docstrings of the ``fromfuncs`` wherever
the documentation of the decorated method is not given.
"""
def _decorator(func):
for fromfunc in fromfuncs:
add_parent_doc(func, fromfunc)
return func
return _decorator
[docs]def staticmethod_inherit_doc(*fromfuncs):
"""
Decorator: Declare the decorated method as a static method and
copy elements from the docstrings of the ``fromfuncs`` wherever
the documentation of the decorated method is not given.
"""
def _decorator(func):
for fromfunc in fromfuncs:
add_parent_doc(func, fromfunc)
docstr = func.__doc__
func = staticmethod(func)
func.__doc__ = docstr
return func
return _decorator
[docs]def strip_private(string:str):
if PRIVATE_STR not in string:
return string
result = ""
for line in string.splitlines(True):
if line.strip()[:len(PRIVATE_STR)] == PRIVATE_STR:
return result
result += line
return result
[docs]def merge(child_str, parent_str, indent_diff=0, joinstr="\n"):
parent_str = adjust_indent(parent_str, indent_diff)
if should_ignore(child_str):
return parent_str
if should_append(child_str):
return joinstr.join([parent_str.rstrip(), re.sub(APPEND_STR, "", child_str, count=1)])
if should_insert(child_str):
return joinstr.join([re.sub(INSERT_STR, "", child_str, count=1).rstrip(), parent_str])
return child_str
[docs]def add_parent_doc(child, parent):
if type(parent) == str:
doc_parent = parent
else:
doc_parent = parent.__doc__
if not doc_parent:
return
doc_child = child.__doc__ if child.__doc__ else ""
if not callable(child) or not (callable(parent)
or type(parent) is staticmethod or type(parent) == str):
indent_child = get_indent_multi(doc_child)
indent_parent = get_indent_multi(doc_parent)
ind_diff = indent_child - indent_parent if doc_child else 0
try:
child.__doc__ = merge(doc_child, strip_private(doc_parent), ind_diff)
except AttributeError:
pass
return
vars_parent, header_parent, footer_parent, indent_parent = split_variables_numpy(doc_parent, True)
vars_child, header_child, footer_child, indent_child = split_variables_numpy(doc_child)
if doc_child:
ind_diff = indent_child - indent_parent
else:
ind_diff = 0
indent_child = indent_parent
header = merge(header_child, header_parent, ind_diff)
footer = merge(footer_child, footer_parent, ind_diff)
variables = list(inspect.signature(child).parameters)
varStr = ""
def add_varStr(var, var_type, var_descr):
return "".join([adjust_indent(" ".join([var, var_type]),
indent_child), var_descr])
for var in variables:
child_var_type, child_var_descr = vars_child.pop(var, [None, None])
parent_var_type, parent_var_descr = vars_parent.pop(var, ["", ""])
var_type = merge(child_var_type, parent_var_type, ind_diff, joinstr=" ")
var_descr = merge(child_var_descr, parent_var_descr, ind_diff, joinstr=" ")
if bool(var_type) and bool(var_descr):
varStr += add_varStr(var, var_type, var_descr)
for var, (child_var_type, child_var_descr) in vars_child.items():
parent_var_type, parent_var_descr = vars_parent.pop(child_var_type, ["", ""])
var_type = merge(child_var_type, parent_var_type, ind_diff, joinstr=" ")
var_descr = merge(child_var_descr, parent_var_descr, ind_diff)
if bool(var_descr):
varStr += add_varStr(var, var_type, var_descr)
if varStr.strip():
varStr = "\n".join([adjust_indent("\nParameters\n----------",
indent_child), varStr])
child.__doc__ = "\n".join([header, varStr, footer])
[docs]def adjust_indent(string:str, difference:int) -> str:
if not string:
if difference > 0:
return " " * difference
else:
return ""
if not difference:
return string
if difference > 0:
diff = " " * difference
return "".join(diff + line for line in string.splitlines(True))
else:
diff = abs(difference)
result = ""
for line in string.splitlines(True):
if get_indent(line) <= diff:
result += line.lstrip()
else:
result += line[diff:]
return result
[docs]def get_indent(string:str) -> int:
return len(string) - len(string.lstrip())
[docs]def get_indent_multi(string:str) -> int:
lines = string.splitlines()
if len(lines) > 1:
return get_indent(lines[1])
else:
return 0
[docs]def split_variables_numpy(docstr:str, stripPrivate:bool=False):
if not docstr.strip():
return {}, docstr, "", 0
lines = docstr.splitlines(True)
header = ""
for i in range(len(lines)-1):
if lines[i].strip() == "Parameters" and lines[i+1].strip() == "----------":
indent = get_indent(lines[i])
i += 2
break
header += lines[i]
else:
return {}, docstr, "", get_indent_multi(docstr)
variables = {}
while i < len(lines)-1 and lines[i].strip():
splitted = lines[i].split(maxsplit=1)
var = splitted[0]
if len(splitted) > 1:
varType = splitted[1]
else:
varType = " "
varStr = ""
i += 1
while i < len(lines) and get_indent(lines[i]) > indent:
varStr += lines[i]
i += 1
if stripPrivate:
varStr = strip_private(varStr)
variables[var] = (varType, strip_lines(varStr))
footer = ""
while i < len(lines):
footer += lines[i]
i += 1
if stripPrivate:
header = strip_private(header)
footer = strip_private(footer)
return variables, header, footer, indent