ArgDoc - Reduce copy/paste in docstrings

This package provides a single class: ArgDoc. ArgDoc is a decorator that will inspect the argspec of any decorated function, method, or class to determine which arguments and keywords are used. It will then modify the docstring of the decorated object to add a parameters list in the Numpy format.

Installation

Via PIP

pip install argdoc

From Source

Clone the repo and install using setup.py:

git clone https://github.com/jsolbrig/argdoc.git
cd argdoc
python setup.py install

Usage

To use ArgDoc as a decorator, it must first be imported within your source code and and an instance must be instantiated:

>>> from argdoc import ArgDoc
>>> arg_doc = ArgDoc()

Next, in order for the decorator to have any effect, positional arguments and keywords must be registered with the ArgDoc instance. If a positional argument or keyword that has not been registered with the ArgDoc instance is encountered in a decorated object’s argspec a KeyError will be raised.

Registering a Positional Argument

Positional arguments can be registered by calling ArgDoc.register_argument() on an instantiated ArgDoc instance. For example, the code below shows how to register an argument called arg1 with type str and a description stating that it is “The first argument”:

>>> arg_doc.register_argument('arg1', str, 'The first argument')
>>> print(arg_doc.arguments['arg1'])
{'type': 'str', 'desc': 'The first argument'}

Note that the typ argument to ArgDoc.register_argument() can be anything. If a type object is passed, typ.__name__ will be used in the documentation while, if anything else is passed, its __str__ representation will be used. To register arg2 whose type is a “list of str”:

>>> arg_doc.register_argument('arg2', 'list of str', 'The second argument')
>>> print(arg_doc.arguments['arg2'])
{'type': 'list of str', 'desc': 'The second argument'}

Note

Maybe this is not the appropriate behavior here. Think about it…

At this point, there are two registered positional arguments:

print(arg_doc.arguments)
{'arg1': {'type': 'str', 'desc': 'The first argument'},
 'arg2': {'type': 'list of str', 'desc': 'The second argument'}}

Registering a Keyword Argument

Registering a keyword argument is similar to registering a positional argument. To register a keyword argument, call ArgDoc.register_keyword() Keyword arguments can be registered by calling ArgDoc.register_keyword() on an instantiated ArgDoc instance. To register a keyword with named def_kw with type int, the description “Keyword with default”, and a default of 1:

>>> arg_doc.register_keyword('def_kw', int, 'Keyword with default defined during registration', default=1)
>>> print(arg_doc.keywords['def_kw'])
{'type': 'int', 'desc': 'Keyword with default defined during registration', 'default': 1}

Providing a default value during registration is optional. If a default value is provided, that default value will be used in the docstring for all decorated objects that include the named keyword in their argspec. If, on the other hand, no default value is provided for a keyword that is found in a decorated object’s argspec, the default value to use in the documentation will be extracted from the object’s argspec for each decorated object:

>>> arg_doc.register_keyword('no_def_kw', int, 'Keyword that gathers default from argspec')
>>> print(arg_doc.keywords['no_def_kw'])
{'type': 'int', 'desc': 'Keyword that gathers default from argspec'}

Note

Setting the default value of a keyword argument during registration does not impact the code, only the documentation. This functionality is provided to allow documentation of keywords whose defaults are not set in the argspec and are, instead, set inside the decorated callable.

At this point, we have two registered keywords:

print(arg_doc.keywords)
{'def_kw': {'type': 'int', 'desc': 'Keyword with default defined during registration', 'default': 1},
 'no_def_kw': {'type': 'int', 'desc': 'Keyword that gathers default from argspec'}}

Decorating a function

To decorate a function, create an instance of ArgDoc and register positional arguments and keywords with the instance as shown above. Then, simply decorate an object with the ArgDoc instance:

@arg_doc()
def test_func(arg1, arg2, def_kw=None, no_def_kw=None):
    '''
    This is a test function that does nothing
    '''
    pass
print(test_func.__doc__)

Note that in the resulting docstring, the default for def_kw was defined during registration of the keyword argument while the default for no_def_kw is gathered from the argspec of the decorated function:

This is a test function that does nothing

Arguments
----------
arg1 : str
    The first argument
arg2 : list of str
    The second argument


Keyword Arguments
-----------------
def_kw : int, optional
    Keyword with default defined during registration Default: 1
no_def_kw : int, optional
    Keyword that gathers default from argspec Default: None

It is not necessary for a decorated function’s argspec to contain all of the registered positional or keyword arguments. Decorating a function with a subset of the registered arguments produces an appropriate docstring:

@arg_doc()
def single_argument(arg1):
    '''
    This function's argspec only contains `arg1`
    '''
    pass

@arg_doc()
def single_keyword(no_def_kw=100):
    '''
    This function's argspec only contains `no_def_kw`
    '''
    pass

print(single_argument.__doc__)
print(single_keyword.__doc__)
This function's argspec only contains `arg1`

Arguments
----------
arg1 : str
    The first argument


This function's argspec only contains `no_def_kw`

Keyword Arguments
-----------------
no_def_kw : int, optional
    Keyword that gathers default from argspec Default: 100

Unregistered Arguments

By default, if a positional or keyword argument is encountered in the decorated function’s argspec that has not been registered with the ArgDoc instance a KeyError will be raised:

@arg_doc()
def bad_argument(badarg):
    '''
    This function has an unregistered argument and will raise a KeyError when decorated
    '''
    pass
Traceback (most recent call last):
...
KeyError: 'Unregistered positional argument `badarg` encountered in argspec of
`<function bad_argument at ...>`'

Ignoring Arguments

In the case where it is undesirable to document a specific positional or keyword argument it can be ignored during the initialization of the ArgDoc instance. To ignore the positional argument ignored_arg and the keyword argument ignored_kw:

>>> arg_doc = ArgDoc(ignore_args=['ignored_arg'], ignore_kws=['ignored_kw'])
>>> arg_doc.register_argument('arg1', str, 'The first argument')
>>> arg_doc.register_argument('arg2', 'list of str', 'The second argument')
>>> arg_doc.register_keyword('def_kw', int, 'Keyword with default defined during registration', default=1)
>>> arg_doc.register_keyword('no_def_kw', int, 'Keyword that gathers default from argspec')
>>> print(arg_doc.ignore_args)
['ignored_arg']
>>> print(arg_doc.ignore_kws)
['ignored_kw']

Decorating a function whose argspec contains ignored arguments results in those arguments being silently omitted from the resulting docstring:

@arg_doc()
def test_func(ignored_arg, arg1, arg2, ignored_kw=None, def_kw=None, no_def_kw=None):
    '''
    In this function's docstring, `ignored_arg` and `ignored_kw` will be omitted
    '''
    pass
print(test_func.__doc__)
In this function's docstring, `ignored_arg` and `ignored_kw` will be omitted

Arguments
----------
arg1 : str
    The first argument
arg2 : list of str
    The second argument


Keyword Arguments
-----------------
def_kw : int, optional
    Keyword with default defined during registration Default: 1
no_def_kw : int, optional
    Keyword that gathers default from argspec Default: None

Documenting Raised Errors

The errors that a function can raise cannot be determined via introspection. To add documentation for the errors that a function can raise, pass them to the decorator via the raises argument:

raised_errors = {'KeyError': 'Raises a KeyError under all circumstances.'}

@arg_doc(raises=raised_errors)
def raise_key_error(arg1):
    '''
    Raises a KeyError
    '''
    raise KeyError()
print(raise_key_error.__doc__)
Raises a KeyError

Arguments
----------
arg1 : str
    The first argument


Raises
------
KeyError
    Raises a KeyError under all circumstances.

Known Issues

  • Currently unable to wrap classmethods.
  • Wrapping staticmethods must be done in the correct order.
  • Currently unable to wrap classes. Please wrap their __init__ and __new__ methods instead.

Documentation Needed

  • Handling of *args and **kwargs
  • Google-style docstrings (Current documentation only covers numpy-style)

Todo

  • In general, further testing is needed
    • Extensive error testing (e.g. what happens when we call the class directly on a non-existing function?)
  • Need to figure out ways to handle returns, yields, etc

API

Indices and tables