Sphinx Extension Page Generation
Objective
Following on from External Data Sphinx Extension, a natural requirement is the ability to automatically generate not just content on a single page with your extension, but also content on wholly new pages!
The story so far
You can find all the code for this example in the repo here: https://github.com/Sam-Martin/sphinx-write-pages-tutorial
If you’re confused about what Sphinx is, why you would want to write an extension, or how to get started, you should check out the start of the External Data Sphinx Extension post.
The basis for this example
My basis for writing this example in this fashion is by following along the approximate process of the built in autosummary extension provided by Sphinx.
You can see the full code for this extension on GitHub. Any mistakes I make in this implementation are my own, and you should defer to the autosummary extension as the canonical way to do this if you want a more complete example.
Writing the extension
We’ll start with the usual Sphinx build and creation of an _ext
subfolder.
$ sphinx-quickstart
Welcome to the Sphinx 4.0.0 quickstart utility.
...
$ mkdir _ext
$ touch _ext/write_pages.py
Our folder now looks like this:
As we open up our new write_pages.py
we’ll need to create four things.
main
functionsetup
functionListPagesDirective
classWritePages
class
The setup
function and <something>Directive
class we’ve seen before, so what are the other two?
The WritePages
class
This is the class that is going to write our .rst
files to disk so that Sphinx can discover them and render the rst within into html/pdf/whatever.
This needs to be seperate from our directive because it needs to run on the builder-inited
event which we will subscribe to with app.connect()
and which does not provide the arguments required to instantiate our directive.
We also use it as the source of truth for our list of files.
Getting lists of files from directives
autosummary does its own parsing of .rst
files in order to pull in the list of files to create from its .. autosummary::
directives. It does this, presumably, because it wants to create .rst
files, which needs to be done before the .rst
files are read by Sphinx, otherwise they won’t get included in the final output.
If it waited until Sphinx had found all instances of its directives in the other .rst
files it would be too late to write its own .rst
files to be read by Sphinx.
Creating custom directive parsing is well outside the scope of this tutorial, so we’ll be using a simple list as a class property on WritePages
as our source list of files to create.
The main
function
This function is a little shim that instantiates our WritePages
class and calls the write_pages
method on it.
In theory we could have the main
function write the pages itself, but we need a source for the list of pages that’s accessible both in the main
function and in our ListPagesDirective
class.
The code
import pathlib
from typing import List
from docutils.frontend import OptionParser
from docutils.nodes import Node
from docutils.utils import new_document
from sphinx.application import Sphinx
from sphinx.parsers import RSTParser
from sphinx.util.docutils import SphinxDirective
class WritePages:
files = [f"file_{i}" for i in range(1, 10)]
relative_path = pathlib.Path(__file__).parent.absolute() / ".."
def write_pages(self) -> None:
for file_name in self.files:
with open(self.relative_path / f"{file_name}.rst", "w") as f:
f.write(f"Test - {file_name}\n==============")
class ListPagesDirective(SphinxDirective):
has_content = True
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.pages = WritePages()
def run(self) -> List[Node]:
rst = ".. toctree::\n"
rst += " :maxdepth: 2\n\n"
for file_name in self.pages.files:
rst += f" {file_name}\n"
return self.parse_rst(rst)
def parse_rst(self, text: str) -> List[Node]:
...
def main(app: Sphinx) -> None:
w = WritePages()
w.write_pages()
def setup(app: Sphinx) -> None:
app.add_directive("list-pages", ListPagesDirective)
app.connect("builder-inited", main)
For brevity I’ve omitted the parse_rst
method contents, again you can see the full implementation in GitHub.
Let’s go through this one step at a time.
- Our
main
will be called, which will - Call our
write_pages
method which will - Write the raw contents of our
.rst
files specified inWritePages.files
- Then our
ListPagesDirective
'srun
method will be called which will get the list of files fromWritePages.files
and turn it into atoctree
, then convert that into docutils nodes withparse_rst
and return it.
Tip: The WritePages.write_pages
method wants the file names with .rst
but when writing the toctree
in ListPagesDirective.run
we want them without the .rst
extension. This always catches me out when writing one of these!
Adding the toctree
We can now use our list-pages
directive to add a toctree!
Write Pages Tutorial
====================
.. list-pages ::
And now we can run
$ make html
Now you’ll notice that your folder now has 10 new files in it!
$ ls -lha
total 120
...
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_1.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_2.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_3.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_4.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_5.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_6.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_7.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_8.rst
-rw-r--r-- 1 sammartin staff 28B 13 Aug 17:53 file_9.rst
And when you open your new _build/html/index.html
file you will see your rendered toctree
and the links to the newly created files!
🎉 Congratulations! You’ve written a Sphinx extension that can organise your auto-generated documentation into multiple sub pages!
On the deletion of files
If you play around with the list of files specified in WritePages.files
you may notice that there’s no mechanism to delete the files. This behaviour reflects that of autosummary.
If you have extremely volatile file lists and are likely to be frequently orphaning pages it may be a good idea to configure your extension to write your files to a dedicated subfolder that’s deleted as part of your make
command.