docfiller#

Note

docfiller assumes Numpy style docstrings

It is common to want to share documentation information across functions/methods. The module docfiller provides a way to do this:

For example, if you have these function:

def a_function(x, y, z):
    """
    A function.

    A longer description


    Parameters
    ----------
    x : float
        An x param
    y : int
        A y param
    z : str
        A z param


    Returns
    -------
    output : float
        An output
    """


def b_function(x, y, z):
    """
    A different function.

    A longer description of b_function


    Parameters
    ----------
    x : float
        An x param
    y : float
        A different y param
    z : str
        A z param


    Returns
    -------
    output : float
        An different output
    """

It would be nice to share info. DocFiller provides a few ways to do this.

First, we can define common docstrings like so:

from module_utilities.docfiller import DocFiller
docstrings = """
Parameters
----------
x : float
    An x param
z : str
    A z param
"""

d = DocFiller.from_docstring(docstrings, combine_keys="parameters")

d.data
{'summary': '',
 'extended_summary': '',
 'parameters': {'x': 'x : float\n    An x param',
  'z': 'z : str\n    A z param'},
 'returns': '',
 'yields': '',
 'notes': '',
 'warnings': '',
 'other_parameters': '',
 'attributes': '',
 'methods': '',
 'references': '',
 'examples': '',
 'x': 'x : float\n    An x param',
 'z': 'z : str\n    A z param'}

This parses the docstring (using numpydoc). Then, we can define functions as:

from textwrap import dedent


@d()
def a_function2(x, y, z):
    """
    A function.

    A longer description


    Parameters
    ----------
    {x}
    y : int
        A y param
    {z}


    Returns
    -------
    output : float
        An output
    """


@d()
def b_function2(x, y, z):
    """
    A different function.

    A longer description of b_function


    Parameters
    ----------
    {x}
    y : float
        A different y param
    {z}


    Returns
    -------
    output : float
        An different output
    """


assert dedent(a_function.__doc__) == a_function2.__doc__
assert dedent(b_function.__doc__) == b_function2.__doc__

print(a_function2.__doc__)
print(b_function2.__doc__)
A function.

A longer description


Parameters
----------
x : float
    An x param
y : int
    A y param
z : str
    A z param


Returns
-------
output : float
    An output


A different function.

A longer description of b_function


Parameters
----------
x : float
    An x param
y : float
    A different y param
z : str
    A z param


Returns
-------
output : float
    An different output

Using templates#

Better still is to generalize the template:

@ (
    d.update(
        summary="A function.",
        extended_summary="""
        A longer description
        """,
    )
    .assign_param("y", "int", "A y param")
    .assign_param("output", "float", "An output")
).dedent().decorate
def a_function3(x, y, z):
    """
    {summary}

    {extended_summary}


    Parameters
    ----------
    {x}
    {y}
    {z}


    Returns
    -------
    {output}
    """


assert a_function3.__doc__ == a_function2.__doc__


@ (
    d.update(
        summary="A different function.",
        extended_summary="A longer description of b_function",
    )
    .assign_param("y", "float", "A different y param")
    .assign_param("output", "float", "An different output")
).dedent()(a_function3)
def b_function3(x, y, z):
    pass


assert b_function3.__doc__ == b_function2.__doc__

Note that for b_function3, we used the template/docstring from a_function3. DocFiller defaults to using the functions docstring for replacement. But you can pass a string or function to use as the template to be filled. Above, we also used DocFiller.assign_param() to create a correctly formatted parameter.

General shared docs#

Another approach is to define shared parameters, with slight variations in there own Docfiller instance, and use this for replacement.

da = DocFiller.from_docstring(
    """
    A function.

    A longer description

    Parameters
    ----------
    y : int
        A y param

    Returns
    -------
    output : float
        An output
    """,
    # this makes parameters and returns available at top level.
    # otherwise, have to access using `parameters.y`, etc.
    combine_keys=["parameters", "returns"],
)

db = DocFiller.from_docstring(
    """
    A different function.

    A longer description of b_function

    Parameters
    ----------
    y : float
        A different y param

    Returns
    -------
    output : float
        An different output
    """,
    combine_keys=["parameters", "returns"],
)
@d.append(da)(a_function3)
def a_function4():
    pass


assert a_function4.__doc__ == a_function2.__doc__


@d.append(db)(a_function3)
def b_function4():
    pass


assert b_function4.__doc__ == b_function4.__doc__

Define parameters/returns that have different key values#

You may find that you can define a host of shared parameter documentation, except that there are a few edge cases. For example, say we have a parameter beta that in some cases is an array, and in others is a float. You could handle this as follows

docstring = """
Parameters
----------
beta_float | beta : float
    A float value for beta
beta_array | beta : array-like
    An array of beta values

x : float
    x parameter
y : float
    y parameter
"""

d = DocFiller.from_docstring(docstring, combine_keys="parameters")

# using `.decorate` uses the decorator without template or parameters


@d.decorate
def func_float(x, y, beta):
    """
    A thing


    Parameters
    ----------
    {x}
    {y}
    {beta_float}
    """


@d.decorate
def func_array(x, y, beta):
    """
    An array thing


    Parameters
    ----------
    {x}
    {y}
    {beta_array}
    """


print(func_float.__doc__)
print(func_array.__doc__)
A thing


Parameters
----------
x : float
    x parameter
y : float
    y parameter
beta : float
    A float value for beta


An array thing


Parameters
----------
x : float
    x parameter
y : float
    y parameter
beta : array-like
    An array of beta values

Or, if everyting else is the same, you could do:

# back to call because passing parameters


@d(beta=d["beta_float"], summary="A thing")
def func_float2(x, y, beta):
    """
    {summary}


    Parameters
    ----------
    {x}
    {y}
    {beta}
    """


@d(func_float2, beta=d["beta_array"], summary="An array thing")
def func_array2(x, y, beta):
    pass


assert func_float2.__doc__ == func_float.__doc__
assert func_array2.__doc__ == func_array.__doc__

Better still, you can use DocFiller.assign_keys():

@d.assign_keys(beta="beta_float")(summary="A thing")
def func_float3(x, y, beta):
    """
    {summary}


    Parameters
    ----------
    {x}
    {y}
    {beta}
    """


@d.assign_keys(beta="beta_array")(func_float3, summary="An array thing")
def func_array3(x, y, beta):
    pass


assert func_float3.__doc__ == func_float.__doc__
assert func_array3.__doc__ == func_array.__doc__
from module_utilities.docfiller import DocFiller

d = DocFiller.from_docstring(
    """
    Parameters
    ----------
    x : int
        x param
    y : float
        y param
    z0 | z : int
        z int param
    z1 | z : float
        z float parma
    """,
    combine_keys="parameters",
)


@d()
def func0():
    """
    Parameters
    ----------
    {x}
    {y}
    {z0}
    """


print(func0.__doc__)
Parameters
----------
x : int
    x param
y : float
    y param
z : int
    z int param

Works with classes#

This also works with classes

expected = """
A summary

A longer summary

Parameters
----------
x : float
    x param
    some other stuff
y : float
    y param

Returns
-------
out : float
    output
"""

d = DocFiller.from_docstring(expected, combine_keys="parameters")
@d()
class hello:
    """
    {summary}

    {extended_summary}

    Parameters
    ----------
    {x}
    {y}

    Returns
    -------
    {returns.out}
    """


assert hello.__doc__ == expected


@d(hello)
class hello2(hello):
    pass


assert hello2.__doc__ == expected

help(hello)
Help on class hello in module __main__:

class hello(builtins.object)
 |  A summary
 |  
 |  A longer summary
 |  
 |  Parameters
 |  ----------
 |  x : float
 |      x param
 |      some other stuff
 |  y : float
 |      y param
 |  
 |  Returns
 |  -------
 |  out : float
 |      output
 |  
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

There are cases where you’d like to update the documentation from one implementation to another. For example, say you have the following functions:

@d.decorate
def func_xy(x, y):
    """
    Add numbers


    Parameters
    ----------
    {x}
    {y}


    Returns
    -------
    output : float
        Sum of input
    """

    return x + y


print(func_xy.__doc__)
Add numbers


Parameters
----------
x : float
    x param
    some other stuff
y : float
    y param


Returns
-------
output : float
    Sum of input

But what if you want a new version that adds three numbers func_xyz(x, y, z)? Most everything is the same as for func_xy above, but we need to add z. For this, we have an interface to docstring-inheritance

@d.inherit(func_xy)
def func_xyz(x, y, z):
    """
    Parameters
    ----------
    z : float
        Another parameter
    """
    return x + y + z


print(func_xyz.__doc__)
Add numbers

Parameters
----------
x : float
    x param
    some other stuff
y : float
    y param
z : float
    Another parameter

Returns
-------
output : float
    Sum of input

For the special case that you want to inherit from methods of a baseclass, you can use the following:

class Base:
    def prop(self):
        """A property"""
        return 1

    @d.decorate
    def meth(self, x):
        """
        A method


        Parameters
        ----------
        {x}


        Returns
        -------
        float
            Double x
        """

        return x * 2


d_from_base = d.factory_inherit_from_parent(Base)


class Derived:
    @d_from_base()
    def prop(self):
        pass

    @d_from_base()
    def meth(self, x, y):
        """
        Parameters
        ----------
        {y}
        """
        return (x + y) * 2
help(Base)
Help on class Base in module __main__:

class Base(builtins.object)
 |  Methods defined here:
 |  
 |  meth(self, x)
 |      A method
 |      
 |      
 |      Parameters
 |      ----------
 |      x : float
 |          x param
 |          some other stuff
 |      
 |      
 |      Returns
 |      -------
 |      float
 |          Double x
 |  
 |  prop(self)
 |      A property
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
help(Derived)
Help on class Derived in module __main__:

class Derived(builtins.object)
 |  Methods defined here:
 |  
 |  meth(self, x, y)
 |      A method
 |      
 |      Parameters
 |      ----------
 |      x : float
 |          x param
 |          some other stuff
 |      y : float
 |          y param
 |      
 |      Returns
 |      -------
 |      float
 |          Double x
 |  
 |  prop(self)
 |      A property
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)