Search code examples
pythonpython-sphinxcompositiondocstring

Extending or overwriting a docstring when composing classes


I have a class MyClass:

class MyClass(object):
    def __init__(self):
        pass

    def my_function(self, x):
        # MyClass.my_function.__doc__ is not writable!
        # Otherwise, I could just set it here.
        Origin.func(self, x)

The class borrows from Origin:

class Origin(object):    
    def func(obj, x):
        """This is a function
        """
        # do stuff
        pass

How can I copy the docstring from Origin.func to MyClass.my_function automatically so that Sphinx Autodoc recognises it? And how can I extend the original docstring by a couple of words?

Edit:

Afaik, I cannot just change __doc__ after the definition of the function since Sphinx would not find it then. Or if it did, where would the "docfix" go?


Solution

  • I'm not clear on exactly how Sphinx works, but assuming it reads from __doc__ rather than parsing the source, there are a number of options.


    Consider the simpler example...

    def add(x, y):
        return x + y
    

    ...which is virtually identical to...

    add = lambda x, y: x + y
    

    In either case, you cannot refer to the symbol add inside its definition, since the symbol is not defined at that point. Nor can you refer to the function object which the symbol add will ultimately refer to, since it hasn't been created yet.

    Therefore, you can only modify add.__doc__ after the symbol has been defined...

    def add(x, y):
        return x + y
    add.__doc__ = 'This is my docstring'
    

    ...but this may be a little more verbose than we'd like.


    Another option is to exploit the fact that the Python decorator syntax...

    @my_decorator
    def add(x, y):
        return x + y
    

    ...is equivalent to...

    def add(x, y):
        return x + y
    add = my_decorator(add)
    

    ...that is, although it's placed before the function definition, it's executed after the function is defined, so you can reference the function object inside the body of the decorator function.


    A decorator function is required to return a callable object, but given that we have no need to change the behavior of the add function, we can just return the argument which is passed in to the decorator, so given the decorator function...

    def set_fixed_docstring(func):
        func.__doc___ = 'This is my docstring'
        return func
    

    ...used like...

    @set_fixed_docstring
    def add(x, y):
        return x + y
    

    ...is equivalent to...

    def add(x, y):
        return x + y
    add = set_fixed_docstring(add)
    

    ...or...

    def add(x, y):
        return x + y
    add.__doc__ = 'This is my docstring'
    add = add
    

    Obviously, a fixed docstring isn't much use here, so we need to parameterize the decorator, which is a little more complex.

    In this instance, we need our decorator function to be callable with a string parameter, and to return a callable object which takes the target function as a parameter.

    The most common way to do this is to define another function within the decorator function, such that the inner function can refer to symbols defined in the outer function. So the function...

    def set_docstring_to(docstring):
        def wrapper(func):
            func.__doc___ = docstring
            return func
        return wrapper
    

    ...used like...

    @set_docstring_to('This is my docstring')
    def add(x, y):
        return x + y
    

    ...is equivalent to...

    def add(x, y):
        return x + y
    add = set_docstring_to('This is my docstring')(add)
    

    ...which boils down to the same code as before...

    def add(x, y):
        return x + y
    add.__doc__ = 'This is my docstring'
    add = add
    

    Putting all this together, if you were to use a decorator like...

    def copy_docstring_from(source):
        def wrapper(func):
            func.__doc__ = source.__doc__
            return func
        return wrapper
    

    ...then you can just do...

    class Origin(object):
        def func(obj, x):
            """This is a function
            """
            # do stuff
            pass
    
    class MyClass(object):
        def __init__(self):
            pass
    
        @copy_docstring_from(Origin.func)
        def my_function(self, x):
            # MyClass.my_function.__doc__ is not writable!
            # Otherwise, I could just set it here.
            Origin.func(self, x)
    

    ...which should achieve the desired result with the minimum amount of code.