diff options
Diffstat (limited to 'doc/development/tutorials/extending_build.rst')
-rw-r--r-- | doc/development/tutorials/extending_build.rst | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/doc/development/tutorials/extending_build.rst b/doc/development/tutorials/extending_build.rst new file mode 100644 index 0000000..a81c84b --- /dev/null +++ b/doc/development/tutorials/extending_build.rst @@ -0,0 +1,379 @@ +.. _tutorial-extend-build: + +Extending the build process +=========================== + +The objective of this tutorial is to create a more comprehensive extension than +that created in :ref:`tutorial-extending-syntax`. +Whereas that guide just covered writing +a custom :term:`role` and :term:`directive`, +this guide covers a more complex extension to the Sphinx build process; +adding 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 to the :mod:`sphinx.ext.todo` extension distributed with Sphinx. + +Overview +-------- + +.. 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 ``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, one for :event:`env-merge-info` + to merge intermediate results from parallel builds, and one for + :event:`env-purge-doc` (the reason for that will be covered later). + + +Prerequisites +------------- + +As with :ref:`tutorial-extending-syntax`, +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`. + +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:`todo.py` + +Here is an example of the folder structure you might obtain: + +.. code-block:: text + + └── source + ├── _ext + │ └── todo.py + ├── _static + ├── conf.py + ├── somefolder + ├── index.rst + ├── somefile.rst + └── someotherfile.rst + + +Writing the extension +--------------------- + +Open :file:`todo.py` and paste the following code in it, all of which we will +explain in detail shortly: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + +This is far more extensive extension than the one detailed in +:ref:`tutorial-extending-syntax`, +however, we will will look at each piece step-by-step to explain what's +happening. + +.. rubric:: The node classes + +Let's start with the node classes: + +.. 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 +``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 + <https://docutils.sourceforge.io/docs/ref/doctree.html>`__ and :ref:`Sphinx + <nodes>`. + +.. attention:: + + It is important to know that while you can extend Sphinx without + leaving your ``conf.py``, if you declare an inherited node right + there, you'll hit an unobvious :py:class:`~pickle.PickleError`. So if + something goes wrong, please make sure that you put inherited nodes + into a separate Python module. + + For more details, see: + + - https://github.com/sphinx-doc/sphinx/issues/6751 + - https://github.com/sphinx-doc/sphinx/issues/1493 + - https://github.com/sphinx-doc/sphinx/issues/1424 + +.. rubric:: 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. + +Looking first at the ``TodolistDirective`` directive: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 24-27 + +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: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 30-53 + +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. 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. + +The node structure that the directive returns looks like this:: + + +--------------------+ + | target node | + +--------------------+ + +--------------------+ + | todo node | + +--------------------+ + \__+--------------------+ + | admonition title | + +--------------------+ + | paragraph | + +--------------------+ + | ... | + +--------------------+ + +.. rubric:: 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. + +Let's look at the event handlers used in the above example. First, the one for +the :event:`env-purge-doc` event: + +.. 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, +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 next handler, for the :event:`env-merge-info` event, is used +during parallel builds. As during parallel builds all threads have +their own ``env``, there's multiple ``todo_all_todos`` lists that need +to be merged: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 64-68 + + +The other handler belongs to the :event:`doctree-resolved` event: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 71-113 + +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 :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 :ref:`previously <tutorial-extending-syntax>`, +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: 116- + +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`. + +If you wish to share your extension across multiple projects or with others, +check out the :ref:`third-party-extensions` section. + + +.. _docutils: https://docutils.sourceforge.io/docs/ +.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH +.. _docutils documentation: https://docutils.sourceforge.io/docs/ref/rst/directives.html |