Jeff Quast

Software Engineer

Technical writing with sphinx

Good technical writing for a suitably complex python project, technical guide, or even a book, will require a supporting structure to sufficiently abstract the commands needed to check and compile it.

Writing a guidebook across a team of writers and developers, we can integrate sphinx, tox, and many other free tools and services. Many features of sphinx are listed on their Welcome page, but this article will focus on:

  • Using tox to automate checking our reStructuredText markup.
  • Cross-platform builds without any Makefile or make.bat.
  • Perform linting of reStructuredText and project code.
  • Create API documentation from """ docstrings """ in code.
  • Uniformly present an introduction on GitHub, PyPi, and readthedocs.org.

Sphinx

To be quick about it, we using the sphinx-quickstart command, specifying ./docs/ when prompted for the folder for sphinx to generate documentation. If we become lost, we can just follow along the First Steps with Sphinx tutorial.

We create an important file pattern: our introduction chapter as file docs/intro.rst, and symlink README.rst to that file. Git manages symlinks in version control perfectly fine, and GitHub follows the symlink target when rendering the top-level README.rst and CONTRIBUTING.rst files

Creating the symlink:

ln -s docs/intro.rst README.rst

And a demo docs/intro.rst:

Introduction
============

Qwack is a toy game.  We're not accepting any contributions,
but you're welcome to read our code!

We bind this to PyPi using a short accessory function in setup.py:

import setuptools

def get_long_description():
    import os, codecs
    fpath_here = os.path.dirname(__file__)
    fpath_readme = os.path.join(fpath_here, 'README.rst')
    return codecs.open(fpath_readme, 'r', 'utf8').read()

setuptools.setup(
    name='qwack',
    packages=['qwack',],
    version=1,
    description='This displays in search results and top of pypi.'
    long_description=get_long_description('README.rst'),
    author='Yours truly,',
)
  • We could modify the get_long_description() function to concatenate additional files, such as a history.rst or CONTRIBUTING.rst to its result string, so that they appear directly on PyPi. PyPi supports only reStructuredText-formatted files for markup.

Tox

Although tox is generally regarded for running tests, the tox workflow is multi-purpose due to its simple workflow similar to a 'containerization' model:

  • create a virtualenv
  • install dependencies.
  • build and install our project by setup.py
  • run 1 or more commands

Because virtualenv is used, execution speed is fast but uniform across all contributing parties. Command execution is defined by an OS-independent .ini file, so Makefile, PowerShell, or bash is avoided. tox then becomes our command dashboard for all contributors and CI systems so that the environment may be reliably reproduced.

tox.ini

With this in mind, we create a tox.ini with testenv target, 'docs':

[tox]
envlist = py35, check, docs

[testenv]
deps = pytest
commands = py.test {posargs:--verbose --verbose} qwack/tests

[testenv:check]
basepython = python3.5
deps = prospector[with_pyroma]
commands = python -m compileall -fq {toxinidir}/qwack
           prospector --with-tool pyroma {toxinidir}

[testenv:docs]
deps = restructuredtext_lint
       doc8
       sphinx
commands = rst-lint README.rst
           doc8 docs/
           sphinx-build -W -b html docs/

[pytest]
norecursedirs = .git .tox

The section [testenv:docs] declares the environment and commands needed to perform a lint check and build HTML documentation. Each tox environment target we specify may be discovered and using tox -l and executed using tox -e command parameters:

$ tox -l
py35
check
docs

Details of the other Tox targets are later described under section, More on Tox.

The 'docs' target

Target [testenv:docs] executes 3 commands:

  1. rst-lint for our README.rst file, ensuring it will not fail to render on PyPi.

  2. doc8 to check style of all of our reStructuredText files in the docs/ sub-folder.

    The doc8 PyPi page fails to render on pypi.python.org due to a markup syntax error. They should have used rst-lint!

  3. sphinx-build to generate HTML documentation of our docs/ sub-folder. Notably, turn warnings into errors is enabled, which informs our CI of a failed build.

Any user with tox installed can perform these actions using:

tox -e docs

Cross-referencing

At its very best, sphinx has astounding support for cross-referencing, whether by referencing functions, classes, or objects in code, other section titles, or even external documentation.

For our example document, we'd like to introduce a simple TOC in docs/index.rst:

=================================
Welcome to Qwack's documentation!
=================================

Contents:

.. toctree::
   :maxdepth: 3

   intro
   api

Each section title up to the 3rd depth level optionally set here is rendered here as a hyperlink. The first title here, "Welcome to Qwack's documentation!" is the first depth level, simply because it is the first one used. Depth levels are defined by a novel identification of title adornment characters.

The contents of two files, intro.rst and api.rst are also referenced here the first is our top-level project README, and the second api.rst:

API Documentation
=================

This is the code documentation for developers, beware!

Begin with the Introduction_ section if you're lost!

main.py
-------

.. automodule:: qwack.main
   :members:
   :undoc-members:

glob expressions may also be used:

.. toctree::
   :maxdepth: 3
   :glob:

   forward
   introduction
   chapters/*
   back_matter/*
   glossery

We can now refer to the target `main.py`_ anywhere else in our docs, and the hyperlinks are managed appropriately by their title. We can also make reference to our API documentation, or even standard python documentation:

This is a context manager for :func:`tty.setcbreak`.

This is made possible with intersphinx. External references made outside of sphinx may also be checked by adding the sphinx-build argument -blinkcheck, this can ensure links to external resources are verified at the time of the build or publication date.

Extensions

Sphinx provides several built-in extensions, and many more can be discovered on pypi. To use them, we define extensions as a python list in docs/conf.py:

extensions = ['sphinx.ext.autodoc',
              'sphinx.ext.intersphinx',
              'sphinx.ext.viewcode',
              'sphinx_paramlinks',
              ]

Sphinx extensions such as sphinx-issues adds domains, allowing markup :ghissue:`29` to refer to pull requests or issue numbers on GitHub. sphinx_paramlinks further extends API documentation to allow referencing function arguments, an additional level of link targets deeper, providing a link for each individual argument:

The :paramref:`Terminal.get_location.timeout` keyword argument can be
specified to return coordinates (-1, -1) after a blocking timeout.

Unlike their "Markdown-flavored" derivatives, these domains allow rendering of sphinx-extended text to be compiled by other reStructuredText-compatible tools even when unsupported. Most requirements of a technical writer may be satisfied by the hundreds of extensions available.

The wonderful thing about integrating with tox.ini, is that we can add 3rd party extensions as deps, abstracting away maintenance and complication, such as a requirements-docs.txt or similar solution, we also contain the installation within the The 'docs' target.

readthedocs.org

The docs/conf.py file created using sphinx-quickstart and publishing to GitHub are the only two requirements needed to use readthedocs.org.

As a bonus, readthedocs.org can create a PDF file for us, which would otherwise require installing LaTeX on our local workstation which can be difficult, even for developers!

If we like the way readthedocs.org looks, we can install the sphinx_rtd_theme dependency and build the same HTML/css format locally.

Advanced Sphinx

What we've covered here is something like a follow-up to the Documenting Your Project Using Sphinx article, so please give it a read if you are new to sphinx or reStructuredText.

You can use sphinx to document many languages other than Python, most certainly the built-in C, C++, and javascript domains and others by extension, such as scala, java, or Go.

More on Tox

Reviewing the tox.ini file listed earlier, we see a pytest command from our testenv section, as suggested by the tox guide section, General tips and tricks.

Notably, we make use of {posargs} so that we can change our test argument signature by escaping with the traditional getopt delimiter --:

tox -epy35 -- --looponfail --exitfirst qwack/tests/core.py

For CI systems, tox recommends building using a pytest target in their jenkins integration page, but tox alone will propagate a non-zero exit code for our build tools, which is sufficient and less complex.

Some of them are rather creative, or used for projects that have nothing to do with python. There are over 22,000 tox.ini file examples on GitHub using the query, 'filename:tox.ini' to explore. A tox file to manage the steps used to develop and publish this article:

[tox]
skipsdist=True

[testenv:build]
deps = docutils
       pygments
whitelist_externals = hugo
commands = hugo -d upload

[testenv:develop]
deps = docutils
       pygments
whitelist_externals = hugo
commands = hugo -w server -d /tmp/hugo-develop

[testenv:publish]
whitelist_externals = rsync
commands = rsync -a upload/ jeffquast.com:jeffquast.com/

And deployed using:

$ tox -ebuild,publish

Code Linting

Returning to our tox.ini file declared earlier in this article, our check target byte-compiles all python files using command python -m compileall. This ensures the code is free of some kinds of syntax errors, quickly.

After byte-compiling the project, prospector is used, prospector front-ends several useful static analysis and style guide-enforcing tools.

Using the optional file, .landscape.yaml, we may declare an explicit list of exclusions to any of the linting or code checking rules that are reported as violated. We might change the "80-column" rule 120, or make exclusions to certain pylint messages:

inherits:
    - strictness_veryhigh

ignore-patterns:
    - (^|/)\..+
    - ^docs/
    - ^build/
    - ^qwack/tests

pep8:
  options:
      max-line-length: 120

pylint:
    options:
        ignored-classes: pytest
        good-names: _,ks,fd
        persistent: no

    disable:
        - protected-access
        - too-few-public-methods
        - star-args
        - wrong-import-order
        - wrong-import-position
        - ungrouped-imports

Contributors then have no doubt about which style rules are enforced, this file becomes a contract among developers and enforced by our CI. The same review process for code changes are used to propose changes.

By using GitHub, the cloud service https://landscape.io can automatically report prospector results, without installing any of these tools on our workstation.

Closing remarks

Although the tools we've used are written in python, we don't require knowing the python language to use them. This article is rendered by a program written in go, for example. By using tox, we reduce the knowledge barrier for contributions and ensure consistent behavior between team members and their windows, mac, or linux server platforms.

By separating our editor and builder, as well as our content from presentation, we allow multiple contributors to work on all of these parts independently. Through version control and workflows offered by basic web services, we achieve more discipline of quality and efficiency through these tools than even the most premium "Office" software suites can offer.