doc: Use 'literalinclude' directive for examples

This avoid duplication and could conceivably let us test this stuff in
code later on.

Signed-off-by: Stephen Finucane <stephen@that.guru>
This commit is contained in:
Stephen Finucane
2019-02-08 17:03:09 +00:00
parent 513d99c316
commit 93081e2fce
4 changed files with 171 additions and 263 deletions

View File

@@ -0,0 +1,13 @@
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)

View File

@@ -0,0 +1,120 @@
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.locale import _
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(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]
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'} # identifies the version of our extension

View File

@@ -55,22 +55,9 @@ Writing the extension
Open :file:`helloworld.py` and paste the following code in it: Open :file:`helloworld.py` and paste the following code in it:
.. code-block:: python .. literalinclude:: examples/helloworld.py
:language: python
from docutils import nodes :linenos:
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)
Some essential things are happening in this example, and you will see them for Some essential things are happening in this example, and you will see them for
all directives. all directives.
@@ -79,13 +66,10 @@ all directives.
Our new directive is declared in the ``HelloWorld`` class. Our new directive is declared in the ``HelloWorld`` class.
.. code-block:: python .. literalinclude:: examples/helloworld.py
:language: python
class HelloWorld(Directive): :linenos:
:lines: 5-9
def run(self):
paragraph_node = nodes.paragraph(text='Hello World!')
return [paragraph_node]
This class extends the docutils_' ``Directive`` class. All extensions that This class extends the docutils_' ``Directive`` class. All extensions that
create directives should extend this class. create directives should extend this class.
@@ -115,10 +99,10 @@ the ``text`` parameter.
This function is a requirement. We use it to plug our new directive into This function is a requirement. We use it to plug our new directive into
Sphinx. Sphinx.
.. code-block:: python .. literalinclude:: examples/helloworld.py
:language: python
def setup(app): :linenos:
app.add_directive("helloworld", HelloWorld) :lines: 12-
The simplest thing you can do it call the :meth:`~Sphinx.add_directive` 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 method, which is what we've done here. For this particular call, the first

View File

@@ -78,127 +78,9 @@ Writing the extension
Open :file:`todo.py` and paste the following code in it, all of which we will Open :file:`todo.py` and paste the following code in it, all of which we will
explain in detail shortly: explain in detail shortly:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
from docutils import nodes :linenos:
from docutils.parsers.rst import Directive
from sphinx.locale import _
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(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]
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'} # identifies the version of our extension
This is far more extensive extension than the one detailed in :doc:`helloworld`, 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 however, we will will look at each piece step-by-step to explain what's
@@ -208,21 +90,10 @@ happening.
Let's start with the node classes: Let's start with the node classes:
.. todo:: Use literal-include .. literalinclude:: examples/todo.py
:language: python
.. code-block:: python :linenos:
:lines: 6-19
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)
Node classes usually don't have to do anything except inherit from the standard Node classes usually don't have to do anything except inherit from the standard
docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from
@@ -246,46 +117,20 @@ the class should have attributes that configure the allowed markup, and a
Looking first at the ``TodolistDirective`` directive: Looking first at the ``TodolistDirective`` directive:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
class TodolistDirective(Directive): :linenos:
:lines: 22-25
def run(self):
return [todolist('')]
It's very simple, creating and returning an instance of our ``todolist`` node It's very simple, creating and returning an instance of our ``todolist`` node
class. The ``TodolistDirective`` directive itself has neither content nor class. The ``TodolistDirective`` directive itself has neither content nor
arguments that need to be handled. That brings us to the ``TodoDirective`` arguments that need to be handled. That brings us to the ``TodoDirective``
directive: directive:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
class TodoDirective(Directive): :linenos:
:lines: 28-53
# 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 Several important things are covered here. First, as you can see, you can refer
to the :ref:`build environment instance <important-objects>` using to the :ref:`build environment instance <important-objects>` using
@@ -337,13 +182,10 @@ here.
Let's look at the event handlers used in the above example. First, the one for Let's look at the event handlers used in the above example. First, the one for
the :event:`env-purge-doc` event: the :event:`env-purge-doc` event:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
def purge_todos(app, env, docname): :linenos:
if not hasattr(env, 'todo_all_todos'): :lines: 56-61
return
env.todo_all_todos = [todo for todo in env.todo_all_todos
if todo['docname'] != docname]
Since we store information from source files in the environment, which is 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, persistent, it may become out of date when the source file changes. Therefore,
@@ -355,48 +197,10 @@ added again during parsing.
The other handler belongs to the :event:`doctree-resolved` event: The other handler belongs to the :event:`doctree-resolved` event:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
def process_todo_nodes(app, doctree, fromdocname): :linenos:
if not app.config.todo_include_todos: :lines: 64-103
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)
The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3 The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3
<build-phases>` and allows custom resolving to be done. The handler we have <build-phases>` and allows custom resolving to be done. The handler we have
@@ -420,23 +224,10 @@ 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 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: the other parts of our extension. Let's look at our ``setup`` function:
.. code-block:: python .. literalinclude:: examples/todo.py
:language: python
def setup(app): :linenos:
app.add_config_value('todo_include_todos', False, 'html') :lines: 106-
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'} # identifies the version of our extension
The calls in this function refer to the classes and functions we added earlier. The calls in this function refer to the classes and functions we added earlier.
What the individual calls do is the following: What the individual calls do is the following: