Aurélien Gâteau

Docutils Snippets

written on Tuesday, March 24, 2015

Last week I had to work with docutils, a Python library to turn reStructuredText (.rst) into documentation. I was using it to extend a Sphinx-based documentation I am setting up. It was quite a frustrating experience: despite loads of search, I could not find any simple, working examples demonstrating Docutils API usage. To save me (and possibly you) some more frustration next time I need to use this library, I am writing down a few examples.

My goal was to create a custom Docutils Directive. A Directive is a class which can be referred to from a .rst file to generate custom content. Its main method is run(), which must return a list of Docutils nodes, representing the custom content. Each node can itself contain other nodes, so run() actually returns a list of node trees.

Available nodes are listed in the Docutils Document Tree. reStructuredText is powerful and expressive, which means creating simple text structures can require quite a lot of nodes, as we shall see.

Let's start with an "Hello World", a simple paragraph:

from docutils import nodes
# ...
class HelloWorld(Directive):
    def run(self):
        para = nodes.paragraph(text='Hello World')
        return [para]

An error I made a lot when starting was to pass the text of the paragraph as a positional argument. I kept writing that:

nodes.paragraph('Hello World')

Instead of this:

nodes.paragraph(text='Hello World')

It does not work because the first argument of paragraph() is the raw source: the string which would produce the paragraph if it came from a .rst document.

Next example, let's create some sections, the equivalent of this .rst source:

Hello
=====
Some text.
A Level 2 Title
---------------
More text.

The code:

class Sections(Directive):
    def run(self):
        section = nodes.section()
        section += nodes.title(text='Hello')
        section += nodes.paragraph(text='Some text.')
        subsection = nodes.section()
        section += subsection
        subsection += nodes.title(text='A Level 2 Title')
        subsection += nodes.paragraph(text='More text.')
        return [section]

Let's now create a bullet list, like the one which would be created by this .rst:

- Apples
- Oranges
- Bananas

This is done with a bullet_list node, which contains list_item nodes, which themselves contain paragraph nodes.

class BulletList(Directive):
    def run(self):
        fruits = ['Apples', 'Oranges', 'Bananas']
        lst = nodes.bullet_list()
        for fruit in fruits:
            item = nodes.list_item()
            lst += item
            item += nodes.paragraph(text=fruit)
        return [lst]

And now for something a bit crazier, what about a table? The rough equivalent of:

============ ========== ======== =====
Product      Unit Price Quantity Price
------------ ---------- -------- -----
Coffee       2          2        4
Orange Juice 3          1        3
Croissant    1.5        2        3
============ ========== ======== =====

This one is a bit more involved:

class TableExample(Directive):
    def run(self):
        header = ('Product', 'Unit Price', 'Quantity', 'Price')
        colwidths = (2, 1, 1, 1)
        data = [
            ('Coffee', '2', '2', '4'),
            ('Orange Juice', '3', '1', '3'),
            ('Croissant', '1.5', '2', '3'),
        ]
        table = nodes.table()
        tgroup = nodes.tgroup(cols=len(header))
        table += tgroup
        for colwidth in colwidths:
            tgroup += nodes.colspec(colwidth=colwidth)
        thead = nodes.thead()
        tgroup += thead
        thead += self.create_table_row(header)
        tbody = nodes.tbody()
        tgroup += tbody
        for data_row in data:
            tbody += self.create_table_row(data_row)
        return [table]
    def create_table_row(self, row_cells):
        row = nodes.row()
        for cell in row_cells:
            entry = nodes.entry()
            row += entry
            entry += nodes.paragraph(text=cell)
        return row

That's it for today, hope this was helpful for some of you. If you want to experiment with this, here is the source code for all these examples: docutils_snippets.py.

PS: I am no Docutils expert, this article may suggest wrong ways to do things, please leave a comment if you notice any error.

This post was tagged docutils, python, rst and sphinx