【问题标题】:Sphinx - insert argument documentation from parent methodSphinx - 从父方法插入参数文档
【发布时间】:2020-01-31 08:16:37
【问题描述】:

我有一些相互继承的类。所有类都包含相同的方法(让我们称之为mymethod),从而子类覆盖基类方法。我想使用sphinx 在所有类中为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)
        [...]


【问题讨论】:

标签: python inheritance python-sphinx restructuredtext


【解决方案1】:

可能不太理想,但也许您可以使用装饰器来扩展文档字符串。例如:

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__ 中搜索它,则可以动态解析覆盖方法,但这意味着装饰器不再可堆叠,因为它不会返回真正的函数。

【讨论】:

    【解决方案2】:

    更新

    我已经根据这个答案中的代码创建了一个 python 包(稍作修改和改进)。可以通过pip install vemomoto_core_tools安装包;基本文档可以在here找到。


    根据@JordanBrière 的回答以及“inherit” method documentation from superclassIs there a way to let classes inherit the documentation of their superclass with sphinx 的回答,我想出了一个更复杂的工具,可以满足我的所有需求。

    特别是:

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

    代码位于此答案的底部。

    用法(一):

    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
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-12-24
      • 2016-04-19
      • 1970-01-01
      • 2022-12-03
      • 2012-06-11
      • 1970-01-01
      • 1970-01-01
      • 2016-03-02
      相关资源
      最近更新 更多