Sphinx - 从父方法插入参数文档
Sphinx - insert argument documentation from parent method
我有一些 class 是相互继承的。所有 classes 都包含相同的方法(让我们称它为 mymethod
),子级由此覆盖基本的 class 方法。我想使用 sphinx 在所有 class 中为 mymethod
生成文档。
假设 mymethod
采用参数 myargument
。此参数对于基本方法和继承方法具有相同的类型和含义。为了尽量减少冗余,我想只为基础 class 编写 myargument
的文档,并在子方法的文档中插入 插入 的文档。也就是说,我不想只简单地引用基础 class,而是在生成文档时动态插入文本。
这能做到吗?怎么样?
请在下面找到一些说明问题的代码。
class BaseClass
def mymethod(myargument):
"""This does something
Params
------
myargument : int
Description of the argument
"""
[...]
class MyClass1(BaseClass):
def mymethod(myargument):
"""This does something
Params
------
[here I would like to insert in the description of ``myargument`` from ``BaseClass.mymethod``]
"""
BaseClass.mymethod(myargument)
[...]
class MyClass2(BaseClass):
def mymethod(myargument, argument2):
"""This does something
Params
------
[here I would like to insert in the description of ``myargument`` in ``BaseClass.mymethod``]
argument2 : int
Description of the additional argument
"""
BaseClass.mymethod(argument)
[...]
可能不理想,但也许您可以使用装饰器来扩展文档字符串。例如:
class extend_docstring:
def __init__(self, method):
self.doc = method.__doc__
def __call__(self, function):
if self.doc is not None:
doc = function.__doc__
function.__doc__ = self.doc
if doc is not None:
function.__doc__ += doc
return function
class BaseClass:
def mymethod(myargument):
"""This does something
Params
------
myargument : int
Description of the argument
"""
[...]
class MyClass1(BaseClass):
@extend_docstring(BaseClass.mymethod)
def mymethod(myargument):
BaseClass.mymethod(myargument)
[...]
class MyClass2(BaseClass):
@extend_docstring(MyClass1.mymethod)
def mymethod(myargument, argument2):
"""argument2 : int
Description of the additional argument
"""
BaseClass.mymethod(argument)
[...]
print('---BaseClass.mymethod---')
print(BaseClass.mymethod.__doc__)
print('---MyClass1.mymethod---')
print(MyClass1.mymethod.__doc__)
print('---MyClass2.mymethod---')
print(MyClass2.mymethod.__doc__)
结果:
---BaseClass.mymethod---
This does something
Params
------
myargument : int
Description of the argument
---MyClass1.mymethod---
This does something
Params
------
myargument : int
Description of the argument
---MyClass2.mymethod---
This does something
Params
------
myargument : int
Description of the argument
argument2 : int
Description of the additional argument
如果您将装饰器设为描述符并在 __get__
中搜索它,则可以动态解析覆盖方法,但这意味着装饰器不再可堆叠,因为它不是 return 真正的装饰器功能。
更新
我已经根据这个答案中的代码创建了一个 python 包(进行了一些小的修改和改进)。该软件包可以通过 pip install vemomoto_core_tools
安装;可以找到基本文档 here.
根据@JordanBrière 的回答和 and Is there a way to let classes inherit the documentation of their superclass with sphinx 的回答,我想出了一个更复杂的工具,可以完成我想要的所有事情。
特别是:
- 如果未为 child 提供,则从 superclass 获取单个参数的文档(numpy 格式)。
- 您可以根据自己的喜好添加方法的新描述并更新参数文档,但未记录的参数将从超级 class.
中记录下来
- 可以替换、插入、添加或忽略文档
- 可以通过在页眉、页脚、类型或参数描述的开头添加标记字符串来控制特定过程
- 以
#
开头的描述将被超class 覆盖
- 以
<!
开头的描述将放在超级class 的描述之前
- 以
!>
开头的描述将放在超级class 的描述后面
- 没有起始标记的描述将替换来自超级 class
的描述
- 超级 classes 可以拥有未转入 children 的文档
- 以
~+~
开头的行之后的行将被继承函数忽略
- 该工具适用于整个 classes(通过元classes)和单个方法(通过装饰器)。两者可以结合。
- 通过装饰器,可以筛选多个方法来定义合适的参数。
- 如果关注的方法捆绑了许多其他方法,这将很有用
代码在这个答案的底部。
用法(1):
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
"""
[...]
@add_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)
[...]
用法(2):
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
"""
@add_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 = ">!"
def should_ignore(string):
return not string or not string.strip() or string.lstrip().startswith(IGNORE_STR)
def should_insert(string):
return string.lstrip().startswith(INSERT_STR)
def should_append(string):
return string.lstrip().startswith(APPEND_STR)
class DocMetaSuperclass(type):
def __new__(mcls, classname, bases, cls_dict):
cls = super().__new__(mcls, classname, bases, cls_dict)
if bases:
for name, member in cls_dict.items():
for base in bases:
if hasattr(base, name):
add_parent_doc(member, getattr(bases[-1], name))
break
return cls
def add_doc(*fromfuncs):
"""
Decorator: Copy the docstring of `fromfunc`
"""
def _decorator(func):
for fromfunc in fromfuncs:
add_parent_doc(func, fromfunc)
return func
return _decorator
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
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, re.sub(APPEND_STR, "", child_str, count=1)])
if should_insert(child_str):
return joinstr.join([re.sub(INSERT_STR, "", child_str, count=1), parent_str])
return child_str
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) == 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 = inspect.getfullargspec(child)[0]
varStr = ""
for var in variables:
child_var_type, child_var_descr = vars_child.get(var, [None, None])
parent_var_type, parent_var_descr = vars_parent.get(var, ["", ""])
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_type) and bool(var_descr):
varStr += "".join([adjust_indent(" ".join([var, var_type]),
indent_child),
var_descr])
if varStr.strip():
varStr = "\n".join([adjust_indent("\nParameters\n----------",
indent_child), varStr])
child.__doc__ = "\n".join([header, varStr, footer])
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
def get_indent(string:str) -> int:
return len(string) - len(string.lstrip())
def get_indent_multi(string:str) -> int:
lines = string.splitlines()
if len(lines) > 1:
return get_indent(lines[1])
else:
return 0
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, 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
我有一些 class 是相互继承的。所有 classes 都包含相同的方法(让我们称它为 mymethod
),子级由此覆盖基本的 class 方法。我想使用 sphinx 在所有 class 中为 mymethod
生成文档。
假设 mymethod
采用参数 myargument
。此参数对于基本方法和继承方法具有相同的类型和含义。为了尽量减少冗余,我想只为基础 class 编写 myargument
的文档,并在子方法的文档中插入 插入 的文档。也就是说,我不想只简单地引用基础 class,而是在生成文档时动态插入文本。
这能做到吗?怎么样?
请在下面找到一些说明问题的代码。
class BaseClass
def mymethod(myargument):
"""This does something
Params
------
myargument : int
Description of the argument
"""
[...]
class MyClass1(BaseClass):
def mymethod(myargument):
"""This does something
Params
------
[here I would like to insert in the description of ``myargument`` from ``BaseClass.mymethod``]
"""
BaseClass.mymethod(myargument)
[...]
class MyClass2(BaseClass):
def mymethod(myargument, argument2):
"""This does something
Params
------
[here I would like to insert in the description of ``myargument`` in ``BaseClass.mymethod``]
argument2 : int
Description of the additional argument
"""
BaseClass.mymethod(argument)
[...]
可能不理想,但也许您可以使用装饰器来扩展文档字符串。例如:
class extend_docstring:
def __init__(self, method):
self.doc = method.__doc__
def __call__(self, function):
if self.doc is not None:
doc = function.__doc__
function.__doc__ = self.doc
if doc is not None:
function.__doc__ += doc
return function
class BaseClass:
def mymethod(myargument):
"""This does something
Params
------
myargument : int
Description of the argument
"""
[...]
class MyClass1(BaseClass):
@extend_docstring(BaseClass.mymethod)
def mymethod(myargument):
BaseClass.mymethod(myargument)
[...]
class MyClass2(BaseClass):
@extend_docstring(MyClass1.mymethod)
def mymethod(myargument, argument2):
"""argument2 : int
Description of the additional argument
"""
BaseClass.mymethod(argument)
[...]
print('---BaseClass.mymethod---')
print(BaseClass.mymethod.__doc__)
print('---MyClass1.mymethod---')
print(MyClass1.mymethod.__doc__)
print('---MyClass2.mymethod---')
print(MyClass2.mymethod.__doc__)
结果:
---BaseClass.mymethod---
This does something
Params
------
myargument : int
Description of the argument
---MyClass1.mymethod---
This does something
Params
------
myargument : int
Description of the argument
---MyClass2.mymethod---
This does something
Params
------
myargument : int
Description of the argument
argument2 : int
Description of the additional argument
如果您将装饰器设为描述符并在 __get__
中搜索它,则可以动态解析覆盖方法,但这意味着装饰器不再可堆叠,因为它不是 return 真正的装饰器功能。
更新
我已经根据这个答案中的代码创建了一个 python 包(进行了一些小的修改和改进)。该软件包可以通过 pip install vemomoto_core_tools
安装;可以找到基本文档 here.
根据@JordanBrière 的回答和
特别是:
- 如果未为 child 提供,则从 superclass 获取单个参数的文档(numpy 格式)。
- 您可以根据自己的喜好添加方法的新描述并更新参数文档,但未记录的参数将从超级 class. 中记录下来
- 可以替换、插入、添加或忽略文档
- 可以通过在页眉、页脚、类型或参数描述的开头添加标记字符串来控制特定过程
- 以
#
开头的描述将被超class 覆盖
- 以
<!
开头的描述将放在超级class 的描述之前
- 以
!>
开头的描述将放在超级class 的描述后面
- 没有起始标记的描述将替换来自超级 class 的描述
- 超级 classes 可以拥有未转入 children 的文档
- 以
~+~
开头的行之后的行将被继承函数忽略
- 以
- 该工具适用于整个 classes(通过元classes)和单个方法(通过装饰器)。两者可以结合。
- 通过装饰器,可以筛选多个方法来定义合适的参数。
- 如果关注的方法捆绑了许多其他方法,这将很有用
代码在这个答案的底部。
用法(1):
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
"""
[...]
@add_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)
[...]
用法(2):
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
"""
@add_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 = ">!"
def should_ignore(string):
return not string or not string.strip() or string.lstrip().startswith(IGNORE_STR)
def should_insert(string):
return string.lstrip().startswith(INSERT_STR)
def should_append(string):
return string.lstrip().startswith(APPEND_STR)
class DocMetaSuperclass(type):
def __new__(mcls, classname, bases, cls_dict):
cls = super().__new__(mcls, classname, bases, cls_dict)
if bases:
for name, member in cls_dict.items():
for base in bases:
if hasattr(base, name):
add_parent_doc(member, getattr(bases[-1], name))
break
return cls
def add_doc(*fromfuncs):
"""
Decorator: Copy the docstring of `fromfunc`
"""
def _decorator(func):
for fromfunc in fromfuncs:
add_parent_doc(func, fromfunc)
return func
return _decorator
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
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, re.sub(APPEND_STR, "", child_str, count=1)])
if should_insert(child_str):
return joinstr.join([re.sub(INSERT_STR, "", child_str, count=1), parent_str])
return child_str
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) == 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 = inspect.getfullargspec(child)[0]
varStr = ""
for var in variables:
child_var_type, child_var_descr = vars_child.get(var, [None, None])
parent_var_type, parent_var_descr = vars_parent.get(var, ["", ""])
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_type) and bool(var_descr):
varStr += "".join([adjust_indent(" ".join([var, var_type]),
indent_child),
var_descr])
if varStr.strip():
varStr = "\n".join([adjust_indent("\nParameters\n----------",
indent_child), varStr])
child.__doc__ = "\n".join([header, varStr, footer])
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
def get_indent(string:str) -> int:
return len(string) - len(string.lstrip())
def get_indent_multi(string:str) -> int:
lines = string.splitlines()
if len(lines) > 1:
return get_indent(lines[1])
else:
return 0
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, 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