2018-11-28 10:27:22 -06:00
|
|
|
Developing a "TODO" extension
|
2018-11-28 10:27:12 -06:00
|
|
|
=============================
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
This section is intended as a walkthrough for the creation of custom extensions.
|
2015-05-15 00:18:19 -05:00
|
|
|
It covers the basics of writing and activating an extension, as well as
|
2008-12-20 17:10:47 -06:00
|
|
|
commonly used features of extensions.
|
|
|
|
|
|
|
|
As an example, we will cover a "todo" extension that adds capabilities to
|
2015-05-15 00:18:19 -05:00
|
|
|
include todo entries in the documentation, and to collect these in a central
|
2008-12-20 17:10:47 -06:00
|
|
|
place. (A similar "todo" extension is distributed with Sphinx.)
|
|
|
|
|
|
|
|
|
|
|
|
Extension Design
|
|
|
|
----------------
|
|
|
|
|
2018-12-22 04:38:12 -06:00
|
|
|
.. note:: To understand the design this extension, refer to
|
|
|
|
:ref:`important-objects` and :ref:`build-phases`.
|
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
We want the extension to add the following to Sphinx:
|
|
|
|
|
|
|
|
* A "todo" directive, containing some content that is marked with "TODO", and
|
|
|
|
only shown in the output if a new config value is set. (Todo entries should
|
|
|
|
not be in the output by default.)
|
|
|
|
|
|
|
|
* A "todolist" directive that creates a list of all todo entries throughout the
|
|
|
|
documentation.
|
|
|
|
|
|
|
|
For that, we will need to add the following elements to Sphinx:
|
|
|
|
|
|
|
|
* New directives, called ``todo`` and ``todolist``.
|
|
|
|
* New document tree nodes to represent these directives, conventionally also
|
|
|
|
called ``todo`` and ``todolist``. We wouldn't need new nodes if the new
|
|
|
|
directives only produced some content representable by existing nodes.
|
|
|
|
* A new config value ``todo_include_todos`` (config value names should start
|
|
|
|
with the extension name, in order to stay unique) that controls whether todo
|
|
|
|
entries make it into the output.
|
|
|
|
* New event handlers: one for the :event:`doctree-resolved` event, to replace
|
|
|
|
the todo and todolist nodes, and one for :event:`env-purge-doc` (the reason
|
|
|
|
for that will be covered later).
|
|
|
|
|
|
|
|
|
|
|
|
The Setup Function
|
|
|
|
------------------
|
|
|
|
|
|
|
|
.. currentmodule:: sphinx.application
|
|
|
|
|
|
|
|
The new elements are added in the extension's setup function. Let us create a
|
|
|
|
new Python module called :file:`todo.py` and add the setup function::
|
|
|
|
|
|
|
|
def setup(app):
|
2015-07-25 08:55:43 -05:00
|
|
|
app.add_config_value('todo_include_todos', False, 'html')
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
app.add_node(todolist)
|
|
|
|
app.add_node(todo,
|
|
|
|
html=(visit_todo_node, depart_todo_node),
|
|
|
|
latex=(visit_todo_node, depart_todo_node),
|
|
|
|
text=(visit_todo_node, depart_todo_node))
|
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
app.add_directive('todo', TodoDirective)
|
|
|
|
app.add_directive('todolist', TodolistDirective)
|
2008-12-20 17:10:47 -06:00
|
|
|
app.connect('doctree-resolved', process_todo_nodes)
|
|
|
|
app.connect('env-purge-doc', purge_todos)
|
|
|
|
|
2014-09-22 04:06:42 -05:00
|
|
|
return {'version': '0.1'} # identifies the version of our extension
|
2014-09-03 09:27:15 -05:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
The calls in this function refer to classes and functions not yet written. What
|
|
|
|
the individual calls do is the following:
|
|
|
|
|
|
|
|
* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the
|
|
|
|
new *config value* ``todo_include_todos``, whose default value should be
|
|
|
|
``False`` (this also tells Sphinx that it is a boolean value).
|
|
|
|
|
2015-07-25 08:55:43 -05:00
|
|
|
If the third argument was ``'html'``, HTML documents would be full rebuild if the
|
2008-12-20 17:10:47 -06:00
|
|
|
config value changed its value. This is needed for config values that
|
2018-12-22 04:38:12 -06:00
|
|
|
influence reading (build :ref:`phase 1 <build-phases>`).
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also
|
|
|
|
can specify visitor functions for each supported output format. These visitor
|
2018-12-22 04:38:12 -06:00
|
|
|
functions are needed when the new nodes stay until :ref:`phase 4 <build-phases>`
|
|
|
|
-- since the ``todolist`` node is always replaced in :ref:`phase 3 <build-phases>`,
|
|
|
|
it doesn't need any.
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
We need to create the two node classes ``todo`` and ``todolist`` later.
|
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
The handler functions are created later.
|
|
|
|
|
|
|
|
* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose
|
|
|
|
name is given by the first argument. The event handler function is called
|
|
|
|
with several arguments which are documented with the event.
|
|
|
|
|
|
|
|
|
|
|
|
The Node Classes
|
|
|
|
----------------
|
|
|
|
|
|
|
|
Let's start with the node classes::
|
|
|
|
|
|
|
|
from docutils import nodes
|
|
|
|
|
|
|
|
class todo(nodes.Admonition, nodes.Element):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class todolist(nodes.General, nodes.Element):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def visit_todo_node(self, node):
|
|
|
|
self.visit_admonition(node)
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
def depart_todo_node(self, node):
|
|
|
|
self.depart_admonition(node)
|
|
|
|
|
|
|
|
Node classes usually don't have to do anything except inherit from the standard
|
|
|
|
docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from
|
|
|
|
``Admonition`` because it should be handled like a note or warning, ``todolist``
|
|
|
|
is just a "general" node.
|
|
|
|
|
2014-01-20 10:21:44 -06:00
|
|
|
.. note::
|
|
|
|
|
|
|
|
Many extensions will not have to create their own node classes and work fine
|
|
|
|
with the nodes already provided by `docutils
|
|
|
|
<http://docutils.sourceforge.net/docs/ref/doctree.html>`__ and :ref:`Sphinx
|
|
|
|
<nodes>`.
|
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
The Directive Classes
|
|
|
|
---------------------
|
2008-12-20 17:10:47 -06:00
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
A directive class is a class deriving usually from
|
2014-01-21 03:32:30 -06:00
|
|
|
:class:`docutils.parsers.rst.Directive`. The directive interface is also
|
|
|
|
covered in detail in the `docutils documentation`_; the important thing is that
|
2015-05-15 00:18:19 -05:00
|
|
|
the class should have attributes that configure the allowed markup,
|
|
|
|
and a ``run`` method that returns a list of nodes.
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
The ``todolist`` directive is quite simple::
|
|
|
|
|
2014-01-20 10:21:44 -06:00
|
|
|
from docutils.parsers.rst import Directive
|
2009-03-07 15:54:36 -06:00
|
|
|
|
|
|
|
class TodolistDirective(Directive):
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
return [todolist('')]
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
An instance of our ``todolist`` node class is created and returned. The
|
|
|
|
todolist directive has neither content nor arguments that need to be handled.
|
|
|
|
|
|
|
|
The ``todo`` directive function looks like this::
|
|
|
|
|
2014-11-26 05:14:31 -06:00
|
|
|
from sphinx.locale import _
|
2008-12-20 17:10:47 -06:00
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
class TodoDirective(Directive):
|
|
|
|
|
|
|
|
# this enables content in the directive
|
|
|
|
has_content = True
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
env = self.state.document.settings.env
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2010-04-17 06:00:19 -05:00
|
|
|
targetid = "todo-%d" % env.new_serialno('todo')
|
2009-03-07 15:54:36 -06:00
|
|
|
targetnode = nodes.target('', '', ids=[targetid])
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2017-01-07 03:35:57 -06:00
|
|
|
todo_node = todo('\n'.join(self.content))
|
|
|
|
todo_node += nodes.title(_('Todo'), _('Todo'))
|
|
|
|
self.state.nested_parse(self.content, self.content_offset, todo_node)
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2009-03-07 15:54:36 -06:00
|
|
|
if not hasattr(env, 'todo_all_todos'):
|
|
|
|
env.todo_all_todos = []
|
|
|
|
env.todo_all_todos.append({
|
|
|
|
'docname': env.docname,
|
|
|
|
'lineno': self.lineno,
|
2017-01-07 03:35:57 -06:00
|
|
|
'todo': todo_node.deepcopy(),
|
2009-03-07 15:54:36 -06:00
|
|
|
'target': targetnode,
|
|
|
|
})
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2017-01-07 03:35:57 -06:00
|
|
|
return [targetnode, todo_node]
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
Several important things are covered here. First, as you can see, you can refer
|
2018-12-22 04:38:12 -06:00
|
|
|
to the :ref:`build environment instance <important-objects>` using ``self.state.document.settings.env``.
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
Then, to act as a link target (from the todolist), the todo directive needs to
|
|
|
|
return a target node in addition to the todo node. The target ID (in HTML, this
|
2014-01-20 10:21:44 -06:00
|
|
|
will be the anchor name) is generated by using ``env.new_serialno`` which
|
|
|
|
returns a new unique integer on each call and therefore leads to unique target
|
|
|
|
names. The target node is instantiated without any text (the first two
|
2009-09-07 15:52:26 -05:00
|
|
|
arguments).
|
2008-12-20 17:10:47 -06:00
|
|
|
|
2017-01-07 03:35:57 -06:00
|
|
|
On creating admonition node, the content body of the directive are parsed using
|
|
|
|
``self.state.nested_parse``. The first argument gives the content body, and
|
|
|
|
the second one gives content offset. The third argument gives the parent node
|
|
|
|
of parsed result, in our case the ``todo`` node.
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
Then, the todo node is added to the environment. This is needed to be able to
|
|
|
|
create a list of all todo entries throughout the documentation, in the place
|
|
|
|
where the author puts a ``todolist`` directive. For this case, the environment
|
|
|
|
attribute ``todo_all_todos`` is used (again, the name should be unique, so it is
|
|
|
|
prefixed by the extension name). It does not exist when a new environment is
|
|
|
|
created, so the directive must check and create it if necessary. Various
|
|
|
|
information about the todo entry's location are stored along with a copy of the
|
|
|
|
node.
|
|
|
|
|
|
|
|
In the last line, the nodes that should be put into the doctree are returned:
|
|
|
|
the target node and the admonition node.
|
|
|
|
|
2009-02-17 17:17:28 -06:00
|
|
|
The node structure that the directive returns looks like this::
|
|
|
|
|
|
|
|
+--------------------+
|
|
|
|
| target node |
|
|
|
|
+--------------------+
|
|
|
|
+--------------------+
|
|
|
|
| todo node |
|
|
|
|
+--------------------+
|
|
|
|
\__+--------------------+
|
|
|
|
| admonition title |
|
|
|
|
+--------------------+
|
|
|
|
| paragraph |
|
|
|
|
+--------------------+
|
|
|
|
| ... |
|
|
|
|
+--------------------+
|
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
The Event Handlers
|
|
|
|
------------------
|
|
|
|
|
|
|
|
Finally, let's look at the event handlers. First, the one for the
|
|
|
|
:event:`env-purge-doc` event::
|
|
|
|
|
|
|
|
def purge_todos(app, env, docname):
|
|
|
|
if not hasattr(env, 'todo_all_todos'):
|
|
|
|
return
|
|
|
|
env.todo_all_todos = [todo for todo in env.todo_all_todos
|
|
|
|
if todo['docname'] != docname]
|
|
|
|
|
|
|
|
Since we store information from source files in the environment, which is
|
|
|
|
persistent, it may become out of date when the source file changes. Therefore,
|
|
|
|
before each source file is read, the environment's records of it are cleared,
|
|
|
|
and the :event:`env-purge-doc` event gives extensions a chance to do the same.
|
|
|
|
Here we clear out all todos whose docname matches the given one from the
|
|
|
|
``todo_all_todos`` list. If there are todos left in the document, they will be
|
|
|
|
added again during parsing.
|
|
|
|
|
|
|
|
The other handler belongs to the :event:`doctree-resolved` event. This event is
|
2018-12-22 04:38:12 -06:00
|
|
|
emitted at the end of :ref:`phase 3 <build-phases>` and allows custom resolving
|
|
|
|
to be done::
|
2008-12-20 17:10:47 -06:00
|
|
|
|
|
|
|
def process_todo_nodes(app, doctree, fromdocname):
|
|
|
|
if not app.config.todo_include_todos:
|
2009-03-09 03:47:10 -05:00
|
|
|
for node in doctree.traverse(todo):
|
2008-12-20 17:10:47 -06:00
|
|
|
node.parent.remove(node)
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
# Replace all todolist nodes with a list of the collected todos.
|
|
|
|
# Augment each todo with a backlink to the original location.
|
|
|
|
env = app.builder.env
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
for node in doctree.traverse(todolist):
|
|
|
|
if not app.config.todo_include_todos:
|
|
|
|
node.replace_self([])
|
|
|
|
continue
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
content = []
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
for todo_info in env.todo_all_todos:
|
|
|
|
para = nodes.paragraph()
|
|
|
|
filename = env.doc2path(todo_info['docname'], base=None)
|
|
|
|
description = (
|
|
|
|
_('(The original entry is located in %s, line %d and can be found ') %
|
|
|
|
(filename, todo_info['lineno']))
|
|
|
|
para += nodes.Text(description, description)
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
# Create a reference
|
|
|
|
newnode = nodes.reference('', '')
|
|
|
|
innernode = nodes.emphasis(_('here'), _('here'))
|
|
|
|
newnode['refdocname'] = todo_info['docname']
|
|
|
|
newnode['refuri'] = app.builder.get_relative_uri(
|
|
|
|
fromdocname, todo_info['docname'])
|
|
|
|
newnode['refuri'] += '#' + todo_info['target']['refid']
|
|
|
|
newnode.append(innernode)
|
|
|
|
para += newnode
|
|
|
|
para += nodes.Text('.)', '.)')
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
# Insert into the todolist
|
|
|
|
content.append(todo_info['todo'])
|
|
|
|
content.append(para)
|
2009-01-10 15:18:18 -06:00
|
|
|
|
2008-12-20 17:10:47 -06:00
|
|
|
node.replace_self(content)
|
|
|
|
|
|
|
|
It is a bit more involved. If our new "todo_include_todos" config value is
|
|
|
|
false, all todo and todolist nodes are removed from the documents.
|
|
|
|
|
|
|
|
If not, todo nodes just stay where and how they are. Todolist nodes are
|
|
|
|
replaced by a list of todo entries, complete with backlinks to the location
|
|
|
|
where they come from. The list items are composed of the nodes from the todo
|
|
|
|
entry and docutils nodes created on the fly: a paragraph for each entry,
|
|
|
|
containing text that gives the location, and a link (reference node containing
|
|
|
|
an italic node) with the backreference. The reference URI is built by
|
|
|
|
``app.builder.get_relative_uri`` which creates a suitable URI depending on the
|
|
|
|
used builder, and appending the todo node's (the target's) ID as the anchor
|
|
|
|
name.
|
|
|
|
|
2013-07-11 15:30:17 -05:00
|
|
|
.. _docutils documentation: http://docutils.sourceforge.net/docs/ref/rst/directives.html
|