XClose

MPHY0021: Research Software Engineering With Python

Home
Menu

Documentation

Documentation is hard

  • Good documentation is hard, and very expensive.
  • Bad documentation is detrimental.
  • Good documentation quickly becomes bad if not kept up-to-date with code changes.
  • Professional companies pay large teams of documentation writers.

Prefer readable code with tests and vignettes

If you don't have the capacity to maintain great documentation, focus on:

  • Readable code
  • Automated tests
  • Small code samples demonstrating how to use the api

Comment-based Documentation tools

Documentation tools can produce extensive documentation about your code by pulling out comments near the beginning of functions, together with the signature, into a web page.

The most popular is Doxygen.

Here are some other documentation tools used in different languages, have a look at the generated and source examples:

Language Name Output example source
Multiple Doxygen Array docs Array docstring source
Python Sphinx numpy.ones docs numpy.ones docstring source
R pkgdown stringr's str_unique stringr's str_unique docstring source
Julia Documnenter.jl ones docs ones docstring source
Fortan FORD arange docs arange docstring source
Rust rustdoc Matrix docs Matrix docstring source

Breathe can be used to make Sphinx and Doxygen work together (good to keep documentation, for example, of a C++ project that includes Python bindings). roxygen2 is another good option for R packages.

Example of using Sphinx

Write some docstrings

We're going to document our "greeter" example from the previous section using docstrings with Sphinx.

There are various conventions for how to write docstrings, but the native Sphinx one doesn't look nice when used with the built in help system.

In writing Greeter, we used the docstring conventions from NumPy. So we use the numpydoc sphinx extension to support these (Note: you will need to install this extension for the later examples to work).

""" 
Generate a greeting string for a person.

Parameters
----------
personal: str
    A given name, such as Will or Jean-Luc

family: str
    A family name, such as Riker or Picard

title: str
    An optional title, such as Captain or Reverend

polite: bool
    True for a formal greeting, False for informal.

Returns
-------
string
    An appropriate greeting
"""

Set up Sphinx

Install Sphinx using the appropiate instructions for your system following the documentation online. (Note that your output and the linked documentation may differ slightly depending on when you installed Sphinx and what version you're using.)

Invoke the sphinx-quickstart command to build Sphinx's configuration file automatically based on questions at the command line:

sphinx-quickstart

Which responds:

Welcome to the Sphinx 4.2.0 quickstart utility.

Please enter values for the following settings (just press Enter to
accept a default value, if one is given in brackets).

Selected root path: .

You have two options for placing the build directory for Sphinx output.
Either, you use a directory "_build" within the root path, or you separate
"source" and "build" directories within the root path.
> Separate source and build directories (y/n) [n]:

The project name will occur in several places in the built documentation.
> Project name: Greetings
> Author name(s): James Hetherington
> Project release []: 0.1

If the documents are to be written in a language other than English,
you can select a language here by its language code. Sphinx will then
translate text that it generates into that language.

For a list of supported codes, see
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.
> Project language [en]:

Creating file ./conf.py.
Creating file ./index.rst.
Creating file ./Makefile.
Creating file ./make.bat.

Finished: An initial directory structure has been created.

You should now populate your master file /tmp/index.rst and create other documentation
source files. Use the Makefile to build the docs, like so:
   make builder
where "builder" is one of the supported builders, e.g. html, latex or linkcheck.

and then look at and adapt the generated config - a file called conf.py in the root of the project - with, for example, the extensions we want to use. This config file contains the project's Sphinx configuration, as Python variables:

#Add any Sphinx extension module names here, as strings. They can be
#extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',  # Support automatic documentation
    'sphinx.ext.coverage', # Automatically check if functions are documented
    'sphinx.ext.mathjax',  # Allow support for algebra
    'sphinx.ext.viewcode', # Include the source code in documentation
    'numpydoc'             # Support NumPy style docstrings
]

To proceed with the example, we'll copy a finished conf.py into our folder, though normally you'll always use sphinx-quickstart

In [1]:
%%writefile greetings/conf.py

import sys
import os

# We need to tell Sphinx where to look for modules
sys.path.insert(0, os.path.abspath('.'))

extensions = [
    'sphinx.ext.autodoc',  # Support automatic documentation
    'sphinx.ext.coverage', # Automatically check if functions are documented
    'sphinx.ext.mathjax',  # Allow support for algebra
    'sphinx.ext.viewcode', # Include the source code in documentation
    'numpydoc'             # Support NumPy style docstrings
]
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = 'Greetings'
copyright = '2014, James Hetherington'
version = '0.1'
release = '0.1'
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
html_theme = 'alabaster'
pygments_style = 'sphinx'
htmlhelp_basename = 'Greetingsdoc'
latex_elements = {
}

latex_documents = [
  ('index', 'Greetings.tex', 'Greetings Documentation',
   'James Hetherington', 'manual'),
]

man_pages = [
    ('index', 'greetings', 'Greetings Documentation',
     ['James Hetherington'], 1)
]

texinfo_documents = [
  ('index', 'Greetings', u'Greetings Documentation',
   'James Hetherington', 'Greetings', 'One line description of project.',
   'Miscellaneous'),
]
Overwriting greetings/conf.py

Define the root documentation page

Sphinx uses RestructuredText another wiki markup format similar to Markdown.

You define an "index.rst" file to contain any preamble text you want. The rest is autogenerated by sphinx-quickstart

In [2]:
%%writefile greetings/index.rst
Welcome to Greetings's documentation!
=====================================

Simple "Hello, James" module developed to teach research software engineering.

.. autofunction:: greetings.greeter.greet
Overwriting greetings/index.rst

 Run sphinx

We can run Sphinx using:

In [3]:
%%bash
cd greetings/
sphinx-build . doc
Running Sphinx v4.3.1
making output directory... done
[autosummary] generating autosummary for: index.rst
building [mo]: targets for 0 po files that are out of date
building [html]: targets for 1 source files that are out of date
updating environment: [new config] 1 added, 0 changed, 0 removed
reading sources... [100%] index

looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
writing output... [100%] index

generating indices... genindex done
highlighting module code... [100%] greetings.greeter

writing additional pages... search done
copying static files... done
copying extra files... done
dumping search index in English (code: en)... done
dumping object inventory... done
build succeeded.

The HTML pages are in doc.

Sphinx output

Sphinx's output is html. We just created a simple single function's documentation, but Sphinx will create multiple nested pages of documentation automatically for many functions.

Doctest - testing your documentation is up to date

doctest is a module included in the standard library. It runs all the code within the docstrings and checks whether the output is what it's claimed on the documentation.

Let's add an example to our greeting function and check it with doctest. We are leaving the output with a small typo (missing the closing quote ') to see what's the type of output we get from doctest.

In [4]:
%%writefile greetings/greetings/greeter.py
def greet(personal, family, title="", polite=False):
    """ Generate a greeting string for a person.

    Parameters
    ----------
    personal: str
        A given name, such as Will or Jean-Luc
    family: str
        A family name, such as Riker or Picard
    title: str
        An optional title, such as Captain or Reverend
    polite: bool
        True for a formal greeting, False for informal.

    Returns
    -------
    string
        An appropriate greeting
        
    Examples
    --------
    >>> from greetings.greeter import greet
    >>> greet("Terry", "Jones")
    'Hey, Terry Jones.
    """

    greeting= "How do you do, " if polite else "Hey, "
    if title:
        greeting += f"{title} "

    greeting += f"{personal} {family}."
    return greeting
Overwriting greetings/greetings/greeter.py
In [5]:
%%bash --no-raise-error
python -m doctest greetings/greetings/greeter.py
**********************************************************************
File "/home/runner/work/rsd-engineeringcourse/rsd-engineeringcourse/ch04packaging/greetings/greetings/greeter.py", line 23, in greeter.greet
Failed example:
    greet("Terry", "Jones")
Expected:
    'Hey, Terry Jones.
Got:
    'Hey, Terry Jones.'
**********************************************************************
1 items had failures:
   1 of   2 in greeter.greet
***Test Failed*** 1 failures.

which clearly identifies a tiny error in our example.

pytest can run the doctest too if you call it as:

pytest --doctest-modules