Python Sphinx-从父方法插入参数文档

Python Sphinx-从父方法插入参数文档,python,inheritance,python-sphinx,restructuredtext,Python,Inheritance,Python Sphinx,Restructuredtext,我有一些相互继承的类。所有类都包含相同的方法(我们称之为mymethod),子类覆盖基类方法。我想在所有类中使用生成mymethod的文档 假设mymethod接受一个参数myargument。此参数对于基本方法和继承方法具有相同的类型和含义。为了尽量减少冗余,我想只为基类编写myargument文档,并在子方法的文档中插入文档。也就是说,我不想只对基类进行简单的引用,而是在生成文档时动态插入文本 这能做到吗?怎么做 下面请查找一些说明问题的代码 class BaseClass def

我有一些相互继承的类。所有类都包含相同的方法(我们称之为
mymethod
),子类覆盖基类方法。我想在所有类中使用生成
mymethod
的文档

假设
mymethod
接受一个参数
myargument
。此参数对于基本方法和继承方法具有相同的类型和含义。为了尽量减少冗余,我想只为基类编写
myargument
文档,并在子方法的文档中插入文档。也就是说,我不想只对基类进行简单的引用,而是在生成文档时动态插入文本

这能做到吗?怎么做

下面请查找一些说明问题的代码

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)
        [...]



可能不太理想,但也许您可以使用装饰程序来扩展docstring。例如:

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

如果将decorator设置为描述符并搜索到
\uuu get\uuuu
中,则可以动态解析重写方法,但这意味着decorator不再可堆叠,因为它不会返回实际函数。

更新

我已经基于这个答案中的代码创建了一个python包(进行了一些小的修改和改进)。该软件包可以通过
pip安装vemomoto_core_工具安装
;可以找到基本文档


根据@jordanbrieère的答案和and的答案,我想出了一个更复杂的工具,可以做我想要的所有事情

特别是:

  • 如果没有为子类提供单参数文档(numpy格式),则从超类获取。
    • 您可以添加方法的新描述,并根据自己的喜好更新参数的文档,但未文档化的参数将从超类中进行文档化
  • 可以替换、插入、添加或忽略文档
    • 可以通过在页眉、页脚、类型或参数描述的开头添加标记字符串来控制特定过程
    • #
      开头的说明将被超类覆盖
    • 开头的说明将放在超级类的说明之前
    • 说明以
      !>将放在超类的描述后面
    • 不带起始标记的描述将替换超类中的描述
  • 超级类可以包含未传递给子类的文档
    • 继承函数将忽略以
      ~+~
      开头的行之后的行
  • 该工具适用于整个类(通过元类)和单个方法(通过装饰器)。这两者可以结合起来
  • 通过decorator,可以筛选多个方法以获得合适的参数定义。
    • 如果关注的方法捆绑了许多其他方法,那么这很有用
代码在这个答案的底部

用法(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)
        [...]
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 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)
        [...]
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

导入检查
进口稀土
忽略_STR=“#”
PRIVATE_STR=“~+~”
插入_STR=“!”
def应忽略(字符串):
返回not string或not string.strip()或string.lstrip().startswith(忽略)
def应插入(字符串):
返回字符串.lstrip().startswith(插入)
def应追加(字符串):
返回字符串.lstrip().startswith(APPEND_STR)
类DocMetaSuperclass(类型):
定义新(mcls、类名、基、cls dict):
cls=super()
如果基于:
对于名称,cls_dict.items()中的成员:
对于基地中的基地:
如果hasattr(基,名称):
添加父文档(成员,getattr(基数[-1],名称))
打破
返回cls
def添加文档(*fromfuncs):
"""
装饰器:复制'fromfunc'的docstring`
"""
def_decorator(func):
对于fromfuncs中的fromfunc:
添加父文档(func,fromfunc)
返回函数
返回装饰器
def strip_private(字符串:str):
如果PRIVATE_STR不在字符串中:
返回字符串
result=“”
对于字符串中的行。拆分行(True):
如果line.strip()[:len(PRIVATE\u STR)]==PRIVATE\u STR:
返回结果
结果+=行
返回结果
def merge(子元素、父元素、缩进差异=0,joinstr=“\n”):
父项缩进=调整缩进(父项缩进,缩进差异)
如果应忽略(子项):
返回父项
如果应追加(子项):
返回joinstr.join([parent\u str,re.sub(APPEND\u str,”,child\u str,count=1)])
如果应插入(子项):
返回joinstr.join([re.sub(INSERT_STR,”,child_STR,count=1),parent_STR])
返回子字符串
def添加父文档(子文档、父文档):
如果类型(父项)==str:
doc\u parent=parent
其他:
文档\父项=父项。\文档__
如果不是doc_父项:
返回
doc\u child=child.\uuuuuu doc\uuuuuuuuuuuuuuuuuuuuif child.\uuuuuuuuuuuuuu doc\uuuuuuuuuuuuuuuuuuuuu
如果不可调用(子级)或不可调用(可调用(父级)或类型(父级)==str):
缩进子对象=获取缩进子对象(文档子对象)
缩进父项=获取缩进多个(文档父项)
ind_diff=缩进子项-如果文档子项为0,则缩进父项
尝试:
子项.\uuuu doc\uuuu=merge(doc\u子项、strip\u private(doc\u父项)、ind\u diff)
除属性错误外:
通过
返回
vars\u parent、header\u parent、footer\u parent、indent\u parent=split\u variables\u numpy(doc\u parent,True)
变量子项、页眉子项、页脚子项、缩进子项=拆分变量子项(文档子项)
如果是doc\u child:
ind_diff=缩进子项-缩进父项
其他:
ind_diff=0
缩进子项=缩进父项
标题=