Merge pull request #6048 from stephenfin/doc-rework-tutorials

Doc rework (development guide, part 1)
This commit is contained in:
Takeshi KOMIYA 2019-02-26 22:51:55 +09:00 committed by GitHub
commit 51db9358fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 963 additions and 322 deletions

View File

@ -0,0 +1,11 @@
:orphan:
Tutorial examples
=================
This directory contains a number of examples used in the tutorials. These are
intended to be increasingly complex to demonstrate the various features of
Sphinx, but should aim to be as complicated as necessary but no more.
Individual sections are referenced by line numbers, meaning if you make changes
to the source files, you should update the references in the documentation
accordingly.

View File

@ -0,0 +1,19 @@
from docutils import nodes
from docutils.parsers.rst import Directive
class HelloWorld(Directive):
def run(self):
paragraph_node = nodes.paragraph(text='Hello World!')
return [paragraph_node]
def setup(app):
app.add_directive("helloworld", HelloWorld)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -0,0 +1,161 @@
from collections import defaultdict
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain
from sphinx.domains import Index
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
class RecipeDirective(ObjectDescription):
"""A custom directive that describes a recipe."""
has_content = True
required_arguments = 1
option_spec = {
'contains': directives.unchanged_required,
}
def handle_signature(self, sig, signode):
signode += addnodes.desc_name(text=sig)
return sig
def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append('recipe' + '-' + sig)
if 'noindex' not in self.options:
ingredients = [
x.strip() for x in self.options.get('contains').split(',')]
recipes = self.env.get_domain('recipe')
recipes.add_recipe(sig, ingredients)
class IngredientIndex(Index):
"""A custom index that creates an ingredient matrix."""
name = 'ingredient'
localname = 'Ingredient Index'
shortname = 'Ingredient'
def generate(self, docnames=None):
content = defaultdict(list)
recipes = {name: (dispname, typ, docname, anchor)
for name, dispname, typ, docname, anchor, _
in self.domain.get_objects()}
recipe_ingredients = self.domain.data['recipe_ingredients']
ingredient_recipes = defaultdict(list)
# flip from recipe_ingredients to ingredient_recipes
for recipe_name, ingredients in recipe_ingredients.items():
for ingredient in ingredients:
ingredient_recipes[ingredient].append(recipe_name)
# convert the mapping of ingredient to recipes to produce the expected
# output, shown below, using the ingredient name as a key to group
#
# name, subtype, docname, anchor, extra, qualifier, description
for ingredient, recipe_names in ingredient_recipes.items():
for recipe_name in recipe_names:
dispname, typ, docname, anchor = recipes[recipe_name]
content[ingredient].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeIndex(Index):
"""A custom index that creates an recipe matrix."""
name = 'recipe'
localname = 'Recipe Index'
shortname = 'Recipe'
def generate(self, docnames=None):
content = defaultdict(list)
# sort the list of recipes in alphabetical order
recipes = self.domain.get_objects()
recipes = sorted(recipes, key=lambda recipe: recipe[0])
# generate the expected output, shown below, from the above using the
# first letter of the recipe as a key to group thing
#
# name, subtype, docname, anchor, extra, qualifier, description
for name, dispname, typ, docname, anchor, _ in recipes:
content[dispname[0].lower()].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeDomain(Domain):
name = 'recipe'
label = 'Recipe Sample'
roles = {
'ref': XRefRole()
}
directives = {
'recipe': RecipeDirective,
}
indices = {
RecipeIndex,
IngredientIndex
}
initial_data = {
'recipes': [], # object list
'recipe_ingredients': {}, # name -> object
}
def get_full_qualified_name(self, node):
return '{}.{}'.format('recipe', node.arguments[0])
def get_objects(self):
for obj in self.data['recipes']:
yield(obj)
def resolve_xref(self, env, fromdocname, builder, typ, target, node,
contnode):
match = [(docname, anchor)
for name, sig, typ, docname, anchor, prio
in self.get_objects() if sig == target]
if len(match) > 0:
todocname = match[0][0]
targ = match[0][1]
return make_refnode(builder, fromdocname, todocname, targ,
contnode, targ)
else:
print('Awww, found nothing')
return None
def add_recipe(self, signature, ingredients):
"""Add a new recipe to the domain."""
name = '{}.{}'.format('recipe', signature)
anchor = 'recipe-{}'.format(signature)
self.data['recipe_ingredients'][name] = ingredients
# name, dispname, type, docname, anchor, priority
self.data['recipes'].append(
(name, signature, 'Recipe', self.env.docname, anchor, 0))
def setup(app):
app.add_domain(RecipeDomain)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -0,0 +1,124 @@
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.locale import _
from sphinx.util.docutils import SphinxDirective
class todo(nodes.Admonition, nodes.Element):
pass
class todolist(nodes.General, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
class TodolistDirective(Directive):
def run(self):
return [todolist('')]
class TodoDirective(SphinxDirective):
# this enables content in the directive
has_content = True
def run(self):
targetid = 'todo-%d' % self.env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(self.env, 'todo_all_todos'):
self.env.todo_all_todos = []
self.env.todo_all_todos.append({
'docname': self.env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
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]
def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
# 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
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
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)
# 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('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
node.replace_self(content)
def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
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))
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -1,85 +1,89 @@
Developing a "Hello world" directive
Developing a "Hello world" extension
====================================
The objective of this tutorial is to create a very basic extension that adds a new
directive that outputs a paragraph containing `hello world`.
The objective of this tutorial is to create a very basic extension that adds a
new directive. This directive will output a paragraph containing "hello world".
Only basic information is provided in this tutorial. For more information,
refer to the :doc:`other tutorials <index>` that go into more
details.
Only basic information is provided in this tutorial. For more information, refer
to the :doc:`other tutorials <index>` that go into more details.
.. warning:: For this extension, you will need some basic understanding of docutils_
.. warning::
For this extension, you will need some basic understanding of docutils_
and Python.
Creating a new extension file
-----------------------------
Your extension file could be in any folder of your project. In our case,
let's do the following:
Overview
--------
#. Create an :file:`_ext` folder in :file:`source`.
We want the extension to add the following to Sphinx:
* A ``helloworld`` directive, that will simply output the text "hello world".
Prerequisites
-------------
We will not be distributing this plugin via `PyPI`_ and will instead include it
as part of an existing project. This means you will need to use an existing
project or create a new one using :program:`sphinx-quickstart`.
We assume you are using separate source (:file:`source`) and build
(:file:`build`) folders. Your extension file could be in any folder of your
project. In our case, let's do the following:
#. Create an :file:`_ext` folder in :file:`source`
#. Create a new Python file in the :file:`_ext` folder called
:file:`helloworld.py`.
:file:`helloworld.py`
Here is an example of the folder structure you might obtain:
Here is an example of the folder structure you might obtain:
.. code-block:: text
.. code-block:: text
└── source
   ├── _ext
  └── helloworld.py
   ├── _static
   ├── conf.py
   ├── somefolder
   ├── index.rst
   ├── somefile.rst
   └── someotherfile.rst
└── source
   ├── _ext
  └── helloworld.py
   ├── _static
   ├── _themes
   ├── conf.py
   ├── somefolder
   ├── somefile.rst
   └── someotherfile.rst
Writing the extension
---------------------
Open :file:`helloworld.py` and paste the following code in it:
.. code-block:: python
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
from docutils import nodes
from docutils.parsers.rst import Directive
Some essential things are happening in this example, and you will see them for
all directives.
.. rubric:: The directive class
class HelloWorld(Directive):
def run(self):
paragraph_node = nodes.paragraph(text='Hello World!')
return [paragraph_node]
Our new directive is declared in the ``HelloWorld`` class.
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
:lines: 5-9
def setup(app):
app.add_directive("helloworld", HelloWorld)
Some essential things are happening in this example, and you will see them
in all directives:
.. rubric:: Directive declaration
Our new directive is declared in the ``HelloWorld`` class, it extends
docutils_' ``Directive`` class. All extensions that create directives
should extend this class.
.. rubric:: ``run`` method
This method is a requirement and it is part of every directive. It contains
the main logic of the directive and it returns a list of docutils nodes to
be processed by Sphinx.
This class extends the docutils_' ``Directive`` class. All extensions that
create directives should extend this class.
.. seealso::
:doc:`todo`
`The docutils documentation on creating directives <docutils directives>`_
.. rubric:: docutils nodes
The ``run`` method returns a list of nodes. Nodes are docutils' way of
representing the content of a document. There are many types of nodes
available: text, paragraph, reference, table, etc.
This class contains a ``run`` method. This method is a requirement and it is
part of every directive. It contains the main logic of the directive and it
returns a list of docutils nodes to be processed by Sphinx. These nodes are
docutils' way of representing the content of a document. There are many types of
nodes available: text, paragraph, reference, table, etc.
.. seealso::
@ -89,74 +93,97 @@ The ``nodes.paragraph`` class creates a new paragraph node. A paragraph
node typically contains some text that we can set during instantiation using
the ``text`` parameter.
.. rubric:: ``setup`` function
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
This function is a requirement. We use it to plug our new directive into
Sphinx.
The simplest thing you can do it call the ``app.add_directive`` method.
.. note::
.. literalinclude:: examples/helloworld.py
:language: python
:linenos:
:lines: 12-
The first argument is the name of the directive itself as used in an rST file.
The simplest thing you can do it call the :meth:`~Sphinx.add_directive` method,
which is what we've done here. For this particular call, the first argument is
the name of the directive itself as used in a reST file. In this case, we would
use ``helloworld``. For example:
In our case, we would use ``helloworld``:
.. code-block:: rst
.. code-block:: rst
Some intro text here...
Some intro text here...
.. helloworld::
.. helloworld::
Some more text here...
Some more text here...
We also return the :ref:`extension metadata <ext-metadata>` that indicates the
version of our extension, along with the fact that it is safe to use the
extension for both parallel reading and writing.
Updating the conf.py file
-------------------------
Using the extension
-------------------
The extension file has to be declared in your :file:`conf.py` file to make
Sphinx aware of it:
The extension has to be declared in your :file:`conf.py` file to make Sphinx
aware of it. There are two steps necessary here:
#. Open :file:`conf.py`. It is in the :file:`source` folder by default.
#. Add ``sys.path.append(os.path.abspath("./_ext"))`` before
the ``extensions`` variable declaration (if it exists).
#. Update or create the ``extensions`` list and add the
extension file name to the list:
#. Add the :file:`_ext` directory to the `Python path`_ using
``sys.path.append``. This should be placed at the top of the file.
.. code-block:: python
#. Update or create the :confval:`extensions` list and add the extension file
name to the list
extensions.append('helloworld')
For example:
You can now use the extension.
.. code-block:: python
.. admonition:: Example
import os
import sys
.. code-block:: rst
sys.path.append(os.path.abspath("./_ext"))
Some intro text here...
extensions = ['helloworld']
.. helloworld::
.. tip::
Some more text here...
We're not distributing this extension as a `Python package`_, we need to
modify the `Python path`_ so Sphinx can find our extension. This is why we
need the call to ``sys.path.append``.
The sample above would generate:
You can now use the extension in a file. For example:
.. code-block:: text
.. code-block:: rst
Some intro text here...
Some intro text here...
Hello World!
.. helloworld::
Some more text here...
Some more text here...
The sample above would generate:
.. code-block:: text
Some intro text here...
Hello World!
Some more text here...
Further reading
---------------
This is the very basic principle of an extension that creates a new directive.
For a more advanced example, refer to :doc:`todo`.
Further reading
---------------
You can create your own nodes if needed, refer to the
:doc:`todo` for more information.
.. _docutils: http://docutils.sourceforge.net/
.. _`docutils nodes`: http://docutils.sourceforge.net/docs/ref/doctree.html
.. _docutils directives: http://docutils.sourceforge.net/docs/howto/rst-directives.html
.. _docutils nodes: http://docutils.sourceforge.net/docs/ref/doctree.html
.. _PyPI: https://pypi.org/
.. _Python package: https://packaging.python.org/
.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH

View File

@ -9,3 +9,4 @@ Refer to the following tutorials to get started with extension development.
helloworld
todo
recipe

View File

@ -0,0 +1,217 @@
Developing a "recipe" extension
===============================
The objective of this tutorial is to illustrate roles, directives and domains.
Once complete, we will be able to use this extension to describe a recipe and
reference that recipe from elsewhere in our documentation.
.. note::
This tutorial is based on a guide first published on `opensource.com`_ and
is provided here with the original author's permission.
.. _opensource.com: https://opensource.com/article/18/11/building-custom-workflows-sphinx
Overview
--------
We want the extension to add the following to Sphinx:
* A ``recipe`` :term:`directive`, containing some content describing the recipe
steps, along with a ``:contains:`` option highlighting the main ingredients
of the recipe.
* A ``ref`` :term:`role`, which provides a cross-reference to the recipe
itself.
* A ``recipe`` :term:`domain`, which allows us to tie together the above role
and domain, along with things like indices.
For that, we will need to add the following elements to Sphinx:
* A new directive called ``recipe``
* New indexes to allow us to reference ingredient and recipes
* A new domain called ``recipe``, which will contain the ``recipe`` directive
and ``ref`` role
Prerequisites
-------------
We need the same setup as in :doc:`the previous extensions <todo>`. This time,
we will be putting out extension in a file called :file:`recipe.py`.
Here is an example of the folder structure you might obtain:
.. code-block:: text
└── source
   ├── _ext
  └── recipe.py
   ├── conf.py
   └── index.rst
Writing the extension
---------------------
Open :file:`recipe.py` and paste the following code in it, all of which we will
explain in detail shortly:
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
Let's look at each piece of this extension step-by-step to explain what's going
on.
.. rubric:: The directive class
The first thing to examine is the ``RecipeDirective`` directive:
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 17-37
Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from
:class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method.
Instead, it derives from :class:`sphinx.directives.ObjectDescription` and
defines ``handle_signature`` and ``add_target_and_index`` methods. This is
because ``ObjectDescription`` is a special-purpose directive that's intended
for describing things like classes, functions, or, in our case, recipes. More
specifically, ``handle_signature`` implements parsing the signature of the
directive and passes on the object's name and type to its superclass, while
``add_taget_and_index`` adds a target (to link to) and an entry to the index
for this node.
We also see that this directive defines ``has_content``, ``required_arguments``
and ``option_spec``. Unlike the ``TodoDirective`` directive added in the
:doc:`previous tutorial <todo>`, this directive takes a single argument, the
recipe name, and an option, ``contains``, in addition to the nested
reStructuredText in the body.
.. rubric:: The index classes
.. currentmodule:: sphinx.domains
.. todo:: Add brief overview of indices
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 40-102
Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`.
They implement custom logic to generate a tuple of values that define the
index. Note that ``RecipeIndex`` is a simple index that has only one entry.
Extending it to cover more object types is not yet part of the code.
Both indices use the method :meth:`Index.generate` to do their work. This
method combines the information from our domain, sorts it, and returns it in a
list structure that will be accepted by Sphinx. This might look complicated but
all it really is is a list of tuples like ``('tomato', 'TomatoSoup', 'test',
'rec-TomatoSoup',...)``. Refer to the :doc:`domain API guide
</extdev/domainapi>` for more information on this API.
.. rubric:: The domain
A Sphinx domain is a specialized container that ties together roles,
directives, and indices, among other things. Let's look at the domain we're
creating here.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 105-155
There are some interesting things to note about this ``recipe`` domain and domains
in general. Firstly, we actually register our directives, roles and indices
here, via the ``directives``, ``roles`` and ``indices`` attributes, rather than
via calls later on in ``setup``. We can also note that we aren't actually
defining a custom role and are instead reusing the
:class:`sphinx.roles.XRefRole` role and defining the
:class:`sphinx.domains.Domain.resolve_xref` method. This method takes two
arguments, ``typ`` and ``target``, which refer to the cross-reference type and
its target name. We'll use ``target`` to resolve our destination from our
domain's ``recipes`` because we currently have only one type of node.
Moving on, we can see that we've defined ``initial_data``. The values defined in
``initial_data`` will be copied to ``env.domaindata[domain_name]`` as the
initial data of the domain, and domain instances can access it via
``self.data``. We see that we have defined two items in ``initial_data``:
``recipes`` and ``recipe2ingredient``. These contain a list of all objects
defined (i.e. all recipes) and a hash that maps a canonical ingredient name to
the list of objects. The way we name objects is common across our extension and
is defined in the ``get_full_qualified_name`` method. For each object created,
the canonical name is ``recipe.<recipename>``, where ``<recipename>`` is the
name the documentation writer gives the object (a recipe). This enables the
extension to use different object types that share the same name. Having a
canonical name and central place for our objects is a huge advantage. Both our
indices and our cross-referencing code use this feature.
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
:doc:`As always <todo>`, the ``setup`` function is a requirement and is used to
hook the various parts of our extension into Sphinx. Let's look at the
``setup`` function for this extension.
.. literalinclude:: examples/recipe.py
:language: python
:linenos:
:lines: 158-
This looks a little different to what we're used to seeing. There are no calls
to :meth:`~Sphinx.add_directive` or even :meth:`~Sphinx.add_role`. Instead, we
have a single call to :meth:`~Sphinx.add_domain` followed by some
initialization of the :ref:`standard domain <domains-std>`. This is because we
had already registered our directives, roles and indexes as part of the
directive itself.
Using the extension
-------------------
You can now use the extension throughout your project. For example:
.. code-block:: rst
:caption: index.rst
Joe's Recipes
=============
Below are a collection of my favourite recipes. I highly recommend the
:recipe:ref:`TomatoSoup` recipe in particular!
.. toctree::
tomato-soup
.. code-block:: rst
:caption: tomato-soup.rst
The recipe contains `tomato` and `cilantro`.
.. recipe:recipe:: TomatoSoup
:contains: tomato cilantro salt pepper
This recipe is a tasty tomato soup, combine all ingredients
and cook.
The important things to note are the use of the ``:recipe:ref:`` role to
cross-reference the recipe actually defined elsewhere (using the
``:recipe:recipe:`` directive.
Further reading
---------------
For more information, refer to the `docutils`_ documentation and
:doc:`/extdev/index`.
.. _docutils: http://docutils.sourceforge.net/docs/

View File

@ -1,114 +1,99 @@
Developing a "TODO" extension
=============================
This section is intended as a walkthrough for the creation of custom extensions.
It covers the basics of writing and activating an extension, as well as
commonly used features of extensions.
As an example, we will cover a "todo" extension that adds capabilities to
include todo entries in the documentation, and to collect these in a central
place. (A similar "todo" extension is distributed with Sphinx.)
The objective of this tutorial is to create a more comprehensive extension than
that created in :doc:`helloworld`. Whereas that guide just covered writing a
custom :term:`directive`, this guide adds multiple directives, along with custom
nodes, additional config values and custom event handlers. To this end, we will
cover a ``todo`` extension that adds capabilities to include todo entries in the
documentation, and to collect these in a central place. This is similar the
``sphinxext.todo`` extension distributed with Sphinx.
Extension Design
----------------
Overview
--------
.. note:: To understand the design this extension, refer to
.. note::
To understand the design of this extension, refer to
:ref:`important-objects` and :ref:`build-phases`.
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 ``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
* 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
------------------
Prerequisites
-------------
.. currentmodule:: sphinx.application
As with :doc:`helloworld`, we will not be distributing this plugin via PyPI so
once again we need a Sphinx project to call this from. You can use an existing
project or create a new one using :program:`sphinx-quickstart`.
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::
We assume you are using separate source (:file:`source`) and build
(:file:`build`) folders. Your extension file could be in any folder of your
project. In our case, let's do the following:
def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
#. Create an :file:`_ext` folder in :file:`source`
#. Create a new Python file in the :file:`_ext` folder called :file:`todo.py`
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))
Here is an example of the folder structure you might obtain:
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
.. code-block:: text
return {'version': '0.1'} # identifies the version of our extension
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).
If the third argument was ``'html'``, HTML documents would be full rebuild if the
config value changed its value. This is needed for config values that
influence reading (build :ref:`phase 1 <build-phases>`).
* :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
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.
We need to create the two node classes ``todo`` and ``todolist`` later.
* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
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.
└── source
   ├── _ext
  └── todo.py
   ├── _static
   ├── conf.py
   ├── somefolder
   ├── index.rst
   ├── somefile.rst
   └── someotherfile.rst
The Node Classes
----------------
Writing the extension
---------------------
Let's start with the node classes::
Open :file:`todo.py` and paste the following code in it, all of which we will
explain in detail shortly:
from docutils import nodes
.. literalinclude:: examples/todo.py
:language: python
:linenos:
class todo(nodes.Admonition, nodes.Element):
pass
This is far more extensive extension than the one detailed in :doc:`helloworld`,
however, we will will look at each piece step-by-step to explain what's
happening.
class todolist(nodes.General, nodes.Element):
pass
.. rubric:: The node classes
def visit_todo_node(self, node):
self.visit_admonition(node)
Let's start with the node classes:
def depart_todo_node(self, node):
self.depart_admonition(node)
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 8-21
Node classes usually don't have to do anything except inherit from the standard
docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from
@ -122,81 +107,54 @@ is just a "general" node.
<http://docutils.sourceforge.net/docs/ref/doctree.html>`__ and :ref:`Sphinx
<nodes>`.
The Directive Classes
---------------------
.. rubric:: The directive classes
A directive class is a class deriving usually from
:class:`docutils.parsers.rst.Directive`. The directive interface is also
:class:`docutils.parsers.rst.Directive`. The directive interface is also
covered in detail in the `docutils documentation`_; the important thing is that
the class should have attributes that configure the allowed markup,
and a ``run`` method that returns a list of nodes.
the class should have attributes that configure the allowed markup, and a
``run`` method that returns a list of nodes.
The ``todolist`` directive is quite simple::
Looking first at the ``TodolistDirective`` directive:
from docutils.parsers.rst import Directive
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 24-27
class TodolistDirective(Directive):
It's very simple, creating and returning an instance of our ``todolist`` node
class. The ``TodolistDirective`` directive itself has neither content nor
arguments that need to be handled. That brings us to the ``TodoDirective``
directive:
def run(self):
return [todolist('')]
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 30-53
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::
from sphinx.locale import _
class TodoDirective(Directive):
# this enables content in the directive
has_content = True
def run(self):
env = self.state.document.settings.env
targetid = "todo-%d" % env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(env, 'todo_all_todos'):
env.todo_all_todos = []
env.todo_all_todos.append({
'docname': env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
Several important things are covered here. First, as you can see, you can refer
to the :ref:`build environment instance <important-objects>` using ``self.state.document.settings.env``.
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
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
arguments).
Several important things are covered here. First, as you can see, we're now
subclassing the :class:`~sphinx.util.docutils.SphinxDirective` helper class
instead of the usual :class:`~docutils.parsers.rst.Directive` class. This
gives us access to the :ref:`build environment instance <important-objects>`
using the ``self.env`` property. Without this, we'd have to use the rather
convoluted ``self.state.document.settings.env``. Then, to act as a link target
(from ``TodolistDirective``), the ``TodoDirective`` directive needs to return a
target node in addition to the ``todo`` node. The target ID (in HTML, this 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 arguments).
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.
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.
of parsed result, in our case the ``todo`` node. Following this, 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.
@ -217,18 +175,20 @@ The node structure that the directive returns looks like this::
| ... |
+--------------------+
.. rubric:: The event handlers
The Event Handlers
------------------
Event handlers are one of Sphinx's most powerful features, providing a way to
do hook into any part of the documentation process. There are many events
provided by Sphinx itself, as detailed in :ref:`the API guide <events>`, and
we're going to use a subset of them here.
Finally, let's look at the event handlers. First, the one for the
:event:`env-purge-doc` event::
Let's look at the event handlers used in the above example. 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]
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 56-61
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,
@ -238,62 +198,144 @@ 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
emitted at the end of :ref:`phase 3 <build-phases>` and allows custom resolving
to be done::
The other handler belongs to the :event:`doctree-resolved` event:
def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 64-103
# 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
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
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)
# 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('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
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
The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3
(resolving) <build-phases>` and allows custom resolving to be done. The handler
we have written for this event is a bit more involved. If the
``todo_include_todos`` config value (which we'll describe shortly) 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.
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 :meth:`sphinx.builders.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.
.. rubric:: The ``setup`` function
.. currentmodule:: sphinx.application
As noted :doc:`previously <helloworld>`, the ``setup`` function is a requirement
and is used to plug directives into Sphinx. However, we also use it to hook up
the other parts of our extension. Let's look at our ``setup`` function:
.. literalinclude:: examples/todo.py
:language: python
:linenos:
:lines: 106-
The calls in this function refer to the classes and functions we added earlier.
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).
If the third argument was ``'html'``, HTML documents would be full rebuild if the
config value changed its value. This is needed for config values that
influence reading (build :ref:`phase 1 (reading) <build-phases>`).
* :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
functions are needed when the new nodes stay until :ref:`phase 4 (writing)
<build-phases>`. Since the ``todolist`` node is always replaced in
:ref:`phase 3 (resolving) <build-phases>`, it doesn't need any.
* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
* 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.
With this, our extension is complete.
Using the extension
-------------------
As before, we need to enable the extension by declaring it in our
:file:`conf.py` file. There are two steps necessary here:
#. Add the :file:`_ext` directory to the `Python path`_ using
``sys.path.append``. This should be placed at the top of the file.
#. Update or create the :confval:`extensions` list and add the extension file
name to the list
In addition, we may wish to set the ``todo_include_todos`` config value. As
noted above, this defaults to ``False`` but we can set it explicitly.
For example:
.. code-block:: python
import os
import sys
sys.path.append(os.path.abspath("./_ext"))
extensions = ['todo']
todo_include_todos = False
You can now use the extension throughout your project. For example:
.. code-block:: rst
:caption: index.rst
Hello, world
============
.. toctree::
somefile.rst
someotherfile.rst
Hello world. Below is the list of TODOs.
.. todolist::
.. code-block:: rst
:caption: somefile.rst
foo
===
Some intro text here...
.. todo:: Fix this
.. code-block:: rst
:caption: someotherfile.rst
bar
===
Some more text here...
.. todo:: Fix that
Because we have configured ``todo_include_todos`` to ``False``, we won't
actually see anything rendered for the ``todo`` and ``todolist`` directives.
However, if we toggle this to true, we will see the output described
previously.
Further reading
---------------
For more information, refer to the `docutils`_ documentation and
:doc:`/extdev/index`.
.. _docutils: http://docutils.sourceforge.net/docs/
.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
.. _docutils documentation: http://docutils.sourceforge.net/docs/ref/rst/directives.html

View File

@ -1195,6 +1195,7 @@ Configuration Variables
See :ref:`cpp-config`.
.. _domains-std:
The Standard Domain
-------------------

View File

@ -91,32 +91,54 @@ class Index:
def generate(self, docnames=None):
# type: (Iterable[str]) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]
"""Return entries for the index given by *name*. If *docnames* is
given, restrict to entries referring to these docnames.
"""Get entries for the index.
The return value is a tuple of ``(content, collapse)``, where *collapse*
is a boolean that determines if sub-entries should start collapsed (for
output formats that support collapsing sub-entries).
If ``docnames`` is given, restrict to entries referring to these
docnames.
*content* is a sequence of ``(letter, entries)`` tuples, where *letter*
is the "heading" for the given *entries*, usually the starting letter.
The return value is a tuple of ``(content, collapse)``:
*entries* is a sequence of single entries, where a single entry is a
sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``.
The items in this sequence have the following meaning:
``collapse``
A boolean that determines if sub-entries should start collapsed (for
output formats that support collapsing sub-entries).
- `name` -- the name of the index entry to be displayed
- `subtype` -- sub-entry related type:
0 -- normal entry
1 -- entry with sub-entries
2 -- sub-entry
- `docname` -- docname where the entry is located
- `anchor` -- anchor for the entry within `docname`
- `extra` -- extra info for the entry
- `qualifier` -- qualifier for the description
- `descr` -- description for the entry
``content``:
A sequence of ``(letter, entries)`` tuples, where ``letter`` is the
"heading" for the given ``entries``, usually the starting letter, and
``entries`` is a sequence of single entries. Each entry is a sequence
``[name, subtype, docname, anchor, extra, qualifier, descr]``. The
items in this sequence have the following meaning:
Qualifier and description are not rendered e.g. in LaTeX output.
``name``
The name of the index entry to be displayed.
``subtype``
The sub-entry related type. One of:
``0``
A normal entry.
``1``
An entry with sub-entries.
``2``
A sub-entry.
``docname``
*docname* where the entry is located.
``anchor``
Anchor for the entry within ``docname``
``extra``
Extra info for the entry.
``qualifier``
Qualifier for the description.
``descr``
Description for the entry.
Qualifier and description are not rendered for some output formats such
as LaTeX.
"""
raise NotImplementedError
@ -318,21 +340,37 @@ class Domain:
def get_objects(self):
# type: () -> Iterable[Tuple[str, str, str, str, str, int]]
"""Return an iterable of "object descriptions", which are tuples with
five items:
"""Return an iterable of "object descriptions".
* `name` -- fully qualified name
* `dispname` -- name to display when searching/linking
* `type` -- object type, a key in ``self.object_types``
* `docname` -- the document where it is to be found
* `anchor` -- the anchor name for the object
* `priority` -- how "important" the object is (determines placement
in search results)
Object descriptions are tuples with six items:
- 1: default priority (placed before full-text matches)
- 0: object is important (placed before default-priority objects)
- 2: object is unimportant (placed after full-text matches)
- -1: object should not show up in search at all
``name``
Fully qualified name.
``dispname``
Name to display when searching/linking.
``type``
Object type, a key in ``self.object_types``.
``docname``
The document where it is to be found.
``anchor``
The anchor name for the object.
``priority``
How "important" the object is (determines placement in search
results). One of:
``1``
Default priority (placed before full-text matches).
``0``
Object is important (placed before default-priority objects).
``2``
Object is unimportant (placed after full-text matches).
``-1``
Object should not show up in search at all.
"""
return []