mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #6048 from stephenfin/doc-rework-tutorials
Doc rework (development guide, part 1)
This commit is contained in:
commit
51db9358fc
11
doc/development/tutorials/examples/README.rst
Normal file
11
doc/development/tutorials/examples/README.rst
Normal 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.
|
19
doc/development/tutorials/examples/helloworld.py
Normal file
19
doc/development/tutorials/examples/helloworld.py
Normal 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,
|
||||
}
|
161
doc/development/tutorials/examples/recipe.py
Normal file
161
doc/development/tutorials/examples/recipe.py
Normal 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,
|
||||
}
|
124
doc/development/tutorials/examples/todo.py
Normal file
124
doc/development/tutorials/examples/todo.py
Normal 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,
|
||||
}
|
@ -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
|
||||
|
@ -9,3 +9,4 @@ Refer to the following tutorials to get started with extension development.
|
||||
|
||||
helloworld
|
||||
todo
|
||||
recipe
|
||||
|
217
doc/development/tutorials/recipe.rst
Normal file
217
doc/development/tutorials/recipe.rst
Normal 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/
|
@ -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
|
||||
|
@ -1195,6 +1195,7 @@ Configuration Variables
|
||||
|
||||
See :ref:`cpp-config`.
|
||||
|
||||
.. _domains-std:
|
||||
|
||||
The Standard Domain
|
||||
-------------------
|
||||
|
@ -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 []
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user