diff options
Diffstat (limited to 'doc/development/tutorials')
-rw-r--r-- | doc/development/tutorials/helloworld.rst | 162 | ||||
-rw-r--r-- | doc/development/tutorials/index.rst | 11 | ||||
-rw-r--r-- | doc/development/tutorials/todo.rst | 299 |
3 files changed, 472 insertions, 0 deletions
diff --git a/doc/development/tutorials/helloworld.rst b/doc/development/tutorials/helloworld.rst new file mode 100644 index 000000000..5ce8db66c --- /dev/null +++ b/doc/development/tutorials/helloworld.rst @@ -0,0 +1,162 @@ +Developing a "Hello world" directive +==================================== + +The objective of this tutorial is to create a very basic extension that adds a new +directive that outputs 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. + +.. 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: + +#. Create an :file:`_ext` folder in :file:`source`. +#. Create a new Python file in the :file:`_ext` folder called + :file:`helloworld.py`. + + Here is an example of the folder structure you might obtain: + + .. code-block:: text + + └── 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 + + 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) + + +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. + +.. seealso:: + + :doc:`todo` + +.. 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. + +.. seealso:: + + `The docutils documentation on nodes <docutils nodes>`_ + +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 + +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:: + + The first argument is the name of the directive itself as used in an rST file. + + In our case, we would use ``helloworld``: + + .. code-block:: rst + + Some intro text here... + + .. helloworld:: + + Some more text here... + + +Updating the conf.py file +------------------------- + +The extension file has to be declared in your :file:`conf.py` file to make +Sphinx aware of it: + +#. 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: + + .. code-block:: python + + extensions.append('helloworld') + +You can now use the extension. + +.. admonition:: Example + + .. code-block:: rst + + Some intro text here... + + .. helloworld:: + + Some more text here... + + The sample above would generate: + + .. code-block:: text + + Some intro text here... + + Hello World! + + Some more text here... + +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
\ No newline at end of file diff --git a/doc/development/tutorials/index.rst b/doc/development/tutorials/index.rst new file mode 100644 index 000000000..cb8dce435 --- /dev/null +++ b/doc/development/tutorials/index.rst @@ -0,0 +1,11 @@ +Extension tutorials +=================== + +Refer to the following tutorials to get started with extension development. + +.. toctree:: + :caption: Directive tutorials + :maxdepth: 1 + + helloworld + todo diff --git a/doc/development/tutorials/todo.rst b/doc/development/tutorials/todo.rst new file mode 100644 index 000000000..e68a39342 --- /dev/null +++ b/doc/development/tutorials/todo.rst @@ -0,0 +1,299 @@ +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.) + + +Extension Design +---------------- + +.. note:: To understand the design 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 "todolist" directive that creates a list of all todo entries throughout the + documentation. + +For that, we will need to add the following elements to Sphinx: + +* New directives, called ``todo`` and ``todolist``. +* New document tree nodes to represent these directives, conventionally also + called ``todo`` and ``todolist``. We wouldn't need new nodes if the new + directives only produced some content representable by existing nodes. +* A new config value ``todo_include_todos`` (config value names should start + with the extension name, in order to stay unique) that controls whether todo + entries make it into the output. +* New event handlers: one for the :event:`doctree-resolved` event, to replace + the todo and todolist nodes, and one for :event:`env-purge-doc` (the reason + for that will be covered later). + + +The Setup Function +------------------ + +.. currentmodule:: sphinx.application + +The new elements are added in the extension's setup function. Let us create a +new Python module called :file:`todo.py` and add the setup function:: + + def setup(app): + 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 + +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. + + +The Node Classes +---------------- + +Let's start with the node classes:: + + from docutils import nodes + + class todo(nodes.Admonition, nodes.Element): + pass + + class todolist(nodes.General, nodes.Element): + pass + + def visit_todo_node(self, node): + self.visit_admonition(node) + + def depart_todo_node(self, node): + self.depart_admonition(node) + +Node classes usually don't have to do anything except inherit from the standard +docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from +``Admonition`` because it should be handled like a note or warning, ``todolist`` +is just a "general" node. + +.. note:: + + Many extensions will not have to create their own node classes and work fine + with the nodes already provided by `docutils + <http://docutils.sourceforge.net/docs/ref/doctree.html>`__ and :ref:`Sphinx + <nodes>`. + + +The Directive Classes +--------------------- + +A directive class is a class deriving usually from +: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 ``todolist`` directive is quite simple:: + + from docutils.parsers.rst import Directive + + class TodolistDirective(Directive): + + def run(self): + return [todolist('')] + +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). + +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. + +In the last line, the nodes that should be put into the doctree are returned: +the target node and the admonition node. + +The node structure that the directive returns looks like this:: + + +--------------------+ + | target node | + +--------------------+ + +--------------------+ + | todo node | + +--------------------+ + \__+--------------------+ + | admonition title | + +--------------------+ + | paragraph | + +--------------------+ + | ... | + +--------------------+ + + +The Event Handlers +------------------ + +Finally, let's look at the event handlers. First, the one for the +:event:`env-purge-doc` event:: + + def purge_todos(app, env, docname): + if not hasattr(env, 'todo_all_todos'): + return + env.todo_all_todos = [todo for todo in env.todo_all_todos + if todo['docname'] != docname] + +Since we store information from source files in the environment, which is +persistent, it may become out of date when the source file changes. Therefore, +before each source file is read, the environment's records of it are cleared, +and the :event:`env-purge-doc` event gives extensions a chance to do the same. +Here we clear out all todos whose docname matches the given one from the +``todo_all_todos`` list. If there are todos left in the document, they will be +added again during parsing. + +The other handler belongs to the :event:`doctree-resolved` event. This event is +emitted at the end of :ref:`phase 3 <build-phases>` and allows custom resolving +to be done:: + + 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) + +It is a bit more involved. If our new "todo_include_todos" config value is +false, all todo and todolist nodes are removed from the documents. + +If not, todo nodes just stay where and how they are. Todolist nodes are +replaced by a list of todo entries, complete with backlinks to the location +where they come from. The list items are composed of the nodes from the todo +entry and docutils nodes created on the fly: a paragraph for each entry, +containing text that gives the location, and a link (reference node containing +an italic node) with the backreference. The reference URI is built by +``app.builder.get_relative_uri`` which creates a suitable URI depending on the +used builder, and appending the todo node's (the target's) ID as the anchor +name. + +.. _docutils documentation: http://docutils.sourceforge.net/docs/ref/rst/directives.html |