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) -> None:
    """
    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) -> None:
    """
    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) -> None:
    """
    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) -> None:
    """
    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) -> None:
    """
    {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) -> None:
    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() -> None:
    pass


assert a_function4.__doc__ == a_function2.__doc__


@d.append(db)(a_function3)
def b_function4() -> None:
    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) -> None:
    """
    A thing


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


@d.decorate
def func_array(x, y, beta) -> None:
    """
    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 everything 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) -> None:
    """
    {summary}


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


@d(func_float2, beta=d["beta_array"], summary="An array thing")
def func_array2(x, y, beta) -> None:
    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) -> None:
    """
    {summary}


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


@d.assign_keys(beta="beta_array")(func_float3, summary="An array thing")
def func_array3(x, y, beta) -> None:
    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 param
    """,
    combine_keys="parameters",
)


@d()
def func0() -> None:
    """
    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

print(Hello2.__doc__)
A summary

A longer summary

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

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

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) -> int:
        """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) -> None:
        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) -> int
 |      A property
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
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) -> None
 |      A property
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object