diff options
Diffstat (limited to 'doc/development')
-rw-r--r-- | doc/development/builders.rst | 34 | ||||
-rw-r--r-- | doc/development/index.rst | 24 | ||||
-rw-r--r-- | doc/development/overview.rst | 32 | ||||
-rw-r--r-- | doc/development/templating.rst | 478 | ||||
-rw-r--r-- | doc/development/theming.rst | 342 | ||||
-rw-r--r-- | doc/development/tutorials/autodoc_ext.rst | 141 | ||||
-rw-r--r-- | doc/development/tutorials/examples/README.rst | 11 | ||||
-rw-r--r-- | doc/development/tutorials/examples/autodoc_intenum.py | 58 | ||||
-rw-r--r-- | doc/development/tutorials/examples/helloworld.py | 19 | ||||
-rw-r--r-- | doc/development/tutorials/examples/recipe.py | 159 | ||||
-rw-r--r-- | doc/development/tutorials/examples/todo.py | 135 | ||||
-rw-r--r-- | doc/development/tutorials/helloworld.rst | 189 | ||||
-rw-r--r-- | doc/development/tutorials/index.rst | 17 | ||||
-rw-r--r-- | doc/development/tutorials/recipe.rst | 227 | ||||
-rw-r--r-- | doc/development/tutorials/todo.rst | 367 |
15 files changed, 2233 insertions, 0 deletions
diff --git a/doc/development/builders.rst b/doc/development/builders.rst new file mode 100644 index 0000000..bb67770 --- /dev/null +++ b/doc/development/builders.rst @@ -0,0 +1,34 @@ +Configuring builders +==================== + +Discover builders by entry point +-------------------------------- + +.. versionadded:: 1.6 + +:term:`builder` extensions can be discovered by means of `entry points`_ so +that they do not have to be listed in the :confval:`extensions` configuration +value. + +Builder extensions should define an entry point in the ``sphinx.builders`` +group. The name of the entry point needs to match your builder's +:attr:`~.Builder.name` attribute, which is the name passed to the +:option:`sphinx-build -b` option. The entry point value should equal the +dotted name of the extension module. Here is an example of how an entry point +for 'mybuilder' can be defined in the extension's ``setup.py`` + +.. code-block:: python + + setup( + # ... + entry_points={ + 'sphinx.builders': [ + 'mybuilder = my.extension.module', + ], + } + ) + +Note that it is still necessary to register the builder using +:meth:`~.Sphinx.add_builder` in the extension's :func:`setup` function. + +.. _entry points: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins diff --git a/doc/development/index.rst b/doc/development/index.rst new file mode 100644 index 0000000..55a31a0 --- /dev/null +++ b/doc/development/index.rst @@ -0,0 +1,24 @@ +========================= +Writing Sphinx Extensions +========================= + +This guide is aimed at giving a quick introduction for those wishing to +develop their own extensions for Sphinx. Sphinx possesses significant +extensibility capabilities including the ability to hook into almost every +point of the build process. If you simply wish to use Sphinx with existing +extensions, refer to :doc:`/usage/index`. For a more detailed discussion of +the extension interface see :doc:`/extdev/index`. + +.. toctree:: + :maxdepth: 2 + + overview + tutorials/index + builders + +.. toctree:: + :caption: Theming + :maxdepth: 2 + + templating + theming diff --git a/doc/development/overview.rst b/doc/development/overview.rst new file mode 100644 index 0000000..df8f5bb --- /dev/null +++ b/doc/development/overview.rst @@ -0,0 +1,32 @@ +Developing extensions overview +============================== + +This page contains general information about developing Sphinx extensions. + +Make an extension depend on another extension +--------------------------------------------- + +Sometimes your extension depends on the functionality of another +Sphinx extension. Most Sphinx extensions are activated in a +project's :file:`conf.py` file, but this is not available to you as an +extension developer. + +.. module:: sphinx.application + :no-index: + +To ensure that another extension is activated as a part of your own extension, +use the :meth:`sphinx.application.Sphinx.setup_extension` method. This will +activate another extension at run-time, ensuring that you have access to its +functionality. + +For example, the following code activates the ``recommonmark`` extension: + +.. code-block:: python + + def setup(app): + app.setup_extension("recommonmark") + +.. note:: + + Since your extension will depend on another, make sure to include + it as a part of your extension's installation requirements. diff --git a/doc/development/templating.rst b/doc/development/templating.rst new file mode 100644 index 0000000..73ae6e5 --- /dev/null +++ b/doc/development/templating.rst @@ -0,0 +1,478 @@ +.. highlight:: html+jinja + +.. _templating: + +========== +Templating +========== + +Sphinx uses the `Jinja <https://jinja.palletsprojects.com/>`_ templating engine +for its HTML templates. Jinja is a text-based engine, inspired by Django +templates, so anyone having used Django will already be familiar with it. It +also has excellent documentation for those who need to make themselves familiar +with it. + + +Do I need to use Sphinx's templates to produce HTML? +---------------------------------------------------- + +No. You have several other options: + +* You can write a :class:`~sphinx.application.TemplateBridge` subclass that + calls your template engine of choice, and set the :confval:`template_bridge` + configuration value accordingly. + +* You can :ref:`write a custom builder <writing-builders>` that derives from + :class:`~sphinx.builders.html.StandaloneHTMLBuilder` and calls your template + engine of choice. + +* You can use the :class:`~sphinxcontrib.serializinghtml.PickleHTMLBuilder` that + produces pickle files with the page contents, and postprocess them using a + custom tool, or use them in your Web application. + + +Jinja/Sphinx Templating Primer +------------------------------ + +The default templating language in Sphinx is Jinja. It's Django/Smarty inspired +and easy to understand. The most important concept in Jinja is :dfn:`template +inheritance`, which means that you can overwrite only specific blocks within a +template, customizing it while also keeping the changes at a minimum. + +To customize the output of your documentation you can override all the templates +(both the layout templates and the child templates) by adding files with the +same name as the original filename into the template directory of the structure +the Sphinx quickstart generated for you. + +Sphinx will look for templates in the folders of :confval:`templates_path` +first, and if it can't find the template it's looking for there, it falls back +to the selected theme's templates. + +A template contains **variables**, which are replaced with values when the +template is evaluated, **tags**, which control the logic of the template and +**blocks** which are used for template inheritance. + +Sphinx's *basic* theme provides base templates with a couple of blocks it will +fill with data. These are located in the :file:`themes/basic` subdirectory of +the Sphinx installation directory, and used by all builtin Sphinx themes. +Templates with the same name in the :confval:`templates_path` override templates +supplied by the selected theme. + +For example, to add a new link to the template area containing related links all +you have to do is to add a new template called ``layout.html`` with the +following contents:: + + {% extends "!layout.html" %} + {% block rootrellink %} + <li><a href="https://project.invalid/">Project Homepage</a> »</li> + {{ super() }} + {% endblock %} + +By prefixing the name of the overridden template with an exclamation mark, +Sphinx will load the layout template from the underlying HTML theme. + +.. important:: + If you override a block, call ``{{ super() }}`` somewhere to render the + block's original content in the extended template -- unless you don't want + that content to show up. + + +Working with the builtin templates +---------------------------------- + +The builtin **basic** theme supplies the templates that all builtin Sphinx +themes are based on. It has the following elements you can override or use: + +Blocks +~~~~~~ + +The following blocks exist in the ``layout.html`` template: + +``doctype`` + The doctype of the output format. By default this is XHTML 1.0 Transitional + as this is the closest to what Sphinx and Docutils generate and it's a good + idea not to change it unless you want to switch to HTML 5 or a different but + compatible XHTML doctype. + +``linktags`` + This block adds a couple of ``<link>`` tags to the head section of the + template. + +``extrahead`` + This block is empty by default and can be used to add extra contents into + the ``<head>`` tag of the generated HTML file. This is the right place to + add references to JavaScript or extra CSS files. + +``relbar1``, ``relbar2`` + This block contains the *relation bar*, the list of related links (the + parent documents on the left, and the links to index, modules etc. on the + right). ``relbar1`` appears before the document, ``relbar2`` after the + document. By default, both blocks are filled; to show the relbar only + before the document, you would override `relbar2` like this:: + + {% block relbar2 %}{% endblock %} + +``rootrellink``, ``relbaritems`` + Inside the relbar there are three sections: The ``rootrellink``, the links + from the documentation and the custom ``relbaritems``. The ``rootrellink`` + is a block that by default contains a list item pointing to the root + document by default, the ``relbaritems`` is an empty block. If you + override them to add extra links into the bar make sure that they are list + items and end with the :data:`reldelim1`. + +``document`` + The contents of the document itself. It contains the block "body" where + the individual content is put by subtemplates like ``page.html``. + + .. note:: + In order for the built-in JavaScript search to show a page preview on + the results page, the document or body content should be wrapped in an + HTML element containing the ``role="main"`` attribute. For example:: + + <div role="main"> + {% block document %}{% endblock %} + </div> + +``sidebar1``, ``sidebar2`` + A possible location for a sidebar. ``sidebar1`` appears before the document + and is empty by default, ``sidebar2`` after the document and contains the + default sidebar. If you want to swap the sidebar location override this and + call the ``sidebar`` helper:: + + {% block sidebar1 %}{{ sidebar() }}{% endblock %} + {% block sidebar2 %}{% endblock %} + + (The ``sidebar2`` location for the sidebar is needed by the ``sphinxdoc.css`` + stylesheet, for example.) + +``sidebarlogo`` + The logo location within the sidebar. Override this if you want to place + some content at the top of the sidebar. + +``footer`` + The block for the footer div. If you want a custom footer or markup before + or after it, override this one. + +The following four blocks are *only* used for pages that do not have assigned a +list of custom sidebars in the :confval:`html_sidebars` config value. Their use +is deprecated in favor of separate sidebar templates, which can be included via +:confval:`html_sidebars`. + +``sidebartoc`` + The table of contents within the sidebar. + + .. deprecated:: 1.0 + +``sidebarrel`` + The relation links (previous, next document) within the sidebar. + + .. deprecated:: 1.0 + +``sidebarsourcelink`` + The "Show source" link within the sidebar (normally only shown if this is + enabled by :confval:`html_show_sourcelink`). + + .. deprecated:: 1.0 + +``sidebarsearch`` + The search box within the sidebar. Override this if you want to place some + content at the bottom of the sidebar. + + .. deprecated:: 1.0 + + +Configuration Variables +~~~~~~~~~~~~~~~~~~~~~~~ + +Inside templates you can set a couple of variables used by the layout template +using the ``{% set %}`` tag: + +.. data:: reldelim1 + + The delimiter for the items on the left side of the related bar. This + defaults to ``' »'`` Each item in the related bar ends with the value + of this variable. + +.. data:: reldelim2 + + The delimiter for the items on the right side of the related bar. This + defaults to ``' |'``. Each item except of the last one in the related bar + ends with the value of this variable. + +Overriding works like this:: + + {% extends "!layout.html" %} + {% set reldelim1 = ' >' %} + +.. data:: script_files + + Add additional script files here, like this:: + + {% set script_files = script_files + ["_static/myscript.js"] %} + + .. deprecated:: 1.8.0 + + Please use ``.Sphinx.add_js_file()`` instead. + +Helper Functions +~~~~~~~~~~~~~~~~ + +Sphinx provides various Jinja functions as helpers in the template. You can use +them to generate links or output multiply used elements. + +.. function:: pathto(document) + + Return the path to a Sphinx document as a URL. Use this to refer to built + documents. + +.. function:: pathto(file, 1) + :no-index: + + Return the path to a *file* which is a filename relative to the root of the + generated output. Use this to refer to static files. + +.. function:: hasdoc(document) + + Check if a document with the name *document* exists. + +.. function:: sidebar() + + Return the rendered sidebar. + +.. function:: relbar() + + Return the rendered relation bar. + +.. function:: warning(message) + + Emit a warning message. + +Global Variables +~~~~~~~~~~~~~~~~ + +These global variables are available in every template and are safe to use. +There are more, but most of them are an implementation detail and might change +in the future. + +.. data:: builder + + The name of the builder (e.g. ``html`` or ``htmlhelp``). + +.. data:: copyright + + The value of :confval:`copyright`. + +.. data:: docstitle + + The title of the documentation (the value of :confval:`html_title`), except + when the "single-file" builder is used, when it is set to ``None``. + +.. data:: embedded + + True if the built HTML is meant to be embedded in some viewing application + that handles navigation, not the web browser, such as for HTML help or Qt + help formats. In this case, the sidebar is not included. + +.. data:: favicon_url + + The relative path to the HTML favicon image from the current document, or + URL to the favicon, or ``''``. + + .. versionadded:: 4.0 + +.. data:: file_suffix + + The value of the builder's :attr:`~.SerializingHTMLBuilder.out_suffix` + attribute, i.e. the file name extension that the output files will get. For + a standard HTML builder, this is usually ``.html``. + +.. data:: has_source + + True if the reST document sources are copied (if :confval:`html_copy_source` + is ``True``). + +.. data:: language + + The value of :confval:`language`. + +.. data:: last_updated + + The build date. + +.. data:: logo_url + + The relative path to the HTML logo image from the current document, or URL + to the logo, or ``''``. + + .. versionadded:: 4.0 + +.. data:: master_doc + + Same as :data:`root_doc`. + + .. versionchanged:: 4.0 + + Renamed to ``root_doc``. + +.. data:: root_doc + + The value of :confval:`root_doc`, for usage with :func:`pathto`. + + .. versionchanged:: 4.0 + + Renamed from ``master_doc``. + +.. data:: pagename + + The "page name" of the current file, i.e. either the document name if the + file is generated from a reST source, or the equivalent hierarchical name + relative to the output directory + (``[directory/]filename_without_extension``). + +.. data:: project + + The value of :confval:`project`. + +.. data:: release + + The value of :confval:`release`. + +.. data:: rellinks + + A list of links to put at the left side of the relbar, next to "next" and + "prev". This usually contains links to the general index and other indices, + such as the Python module index. If you add something yourself, it must be a + tuple ``(pagename, link title, accesskey, link text)``. + +.. data:: shorttitle + + The value of :confval:`html_short_title`. + +.. data:: show_source + + True if :confval:`html_show_sourcelink` is ``True``. + +.. data:: sphinx_version + + The version of Sphinx used to build represented as a string for example "3.5.1". + +.. data:: sphinx_version_tuple + + The version of Sphinx used to build represented as a tuple of five elements. + For Sphinx version 3.5.1 beta 3 this would be ``(3, 5, 1, 'beta', 3)``. + The fourth element can be one of: ``alpha``, ``beta``, ``rc``, ``final``. + ``final`` always has 0 as the last element. + + .. versionadded:: 4.2 + +.. data:: docutils_version_info + + The version of Docutils used to build represented as a tuple of five elements. + For Docutils version 0.16.1 beta 2 this would be ``(0, 16, 1, 'beta', 2)``. + The fourth element can be one of: ``alpha``, ``beta``, ``candidate``, ``final``. + ``final`` always has 0 as the last element. + + .. versionadded:: 5.0.2 + +.. data:: styles + + A list of the names of the main stylesheets as given by the theme or + :confval:`html_style`. + + .. versionadded:: 5.1 + +.. data:: title + + The title of the current document, as used in the ``<title>`` tag. + +.. data:: use_opensearch + + The value of :confval:`html_use_opensearch`. + +.. data:: version + + The value of :confval:`version`. + + +In addition to these values, there are also all **theme options** available +(prefixed by ``theme_``), as well as the values given by the user in +:confval:`html_context`. + +In documents that are created from source files (as opposed to +automatically-generated files like the module index, or documents that already +are in HTML form), these variables are also available: + +.. data:: body + + A string containing the content of the page in HTML form as produced by the + HTML builder, before the theme is applied. + +.. data:: display_toc + + A boolean that is True if the toc contains more than one entry. + +.. data:: meta + + Document metadata (a dictionary), see :ref:`metadata`. + +.. data:: metatags + + A string containing the page's HTML :dudir:`meta` tags. + +.. data:: next + + The next document for the navigation. This variable is either false or has + two attributes `link` and `title`. The title contains HTML markup. For + example, to generate a link to the next page, you can use this snippet:: + + {% if next %} + <a href="{{ next.link|e }}">{{ next.title }}</a> + {% endif %} + +.. data:: page_source_suffix + + The suffix of the file that was rendered. Since we support a list of + :confval:`source_suffix`, this will allow you to properly link to the + original source file. + +.. data:: parents + + A list of parent documents for navigation, structured like the :data:`next` + item. + +.. data:: prev + + Like :data:`next`, but for the previous page. + +.. data:: sourcename + + The name of the copied source file for the current document. This is only + nonempty if the :confval:`html_copy_source` value is ``True``. + This has empty value on creating automatically-generated files. + +.. data:: toc + + The local table of contents for the current page, rendered as HTML bullet + lists. + +.. data:: toctree + + A callable yielding the global TOC tree containing the current page, rendered + as HTML bullet lists. Optional keyword arguments: + + ``collapse`` + If true, all TOC entries that are not ancestors of the current page are + collapsed. + ``True`` by default. + + ``maxdepth`` + The maximum depth of the tree. Set it to ``-1`` to allow unlimited depth. + Defaults to the max depth selected in the toctree directive. + + ``titles_only`` + If true, put only top-level document titles in the tree. + ``False`` by default. + + ``includehidden`` + If true, the ToC tree will also contain hidden entries. + ``False`` by default. diff --git a/doc/development/theming.rst b/doc/development/theming.rst new file mode 100644 index 0000000..538dcaf --- /dev/null +++ b/doc/development/theming.rst @@ -0,0 +1,342 @@ +HTML theme development +====================== + +.. versionadded:: 0.6 + +.. note:: + + This document provides information about creating your own theme. If you + simply wish to use a pre-existing HTML themes, refer to + :doc:`/usage/theming`. + +Sphinx supports changing the appearance of its HTML output via *themes*. A +theme is a collection of HTML templates, stylesheet(s) and other static files. +Additionally, it has a configuration file which specifies from which theme to +inherit, which highlighting style to use, and what options exist for customizing +the theme's look and feel. + +Themes are meant to be project-unaware, so they can be used for different +projects without change. + +.. note:: + + See :ref:`dev-extensions` for more information that may + be helpful in developing themes. + + +Creating themes +--------------- + +Themes take the form of either a directory or a zipfile (whose name is the +theme name), containing the following: + +* A :file:`theme.conf` file. +* HTML templates, if needed. +* A ``static/`` directory containing any static files that will be copied to the + output static directory on build. These can be images, styles, script files. + +The :file:`theme.conf` file is in INI format [1]_ (readable by the standard +Python :mod:`configparser` module) and has the following structure: + +.. sourcecode:: ini + + [theme] + inherit = base theme + stylesheet = main CSS name + pygments_style = stylename + sidebars = localtoc.html, relations.html, sourcelink.html, searchbox.html + + [options] + variable = default value + +* The **inherit** setting gives the name of a "base theme", or ``none``. The + base theme will be used to locate missing templates (most themes will not have + to supply most templates if they use ``basic`` as the base theme), its options + will be inherited, and all of its static files will be used as well. If you + want to also inherit the stylesheet, include it via CSS' ``@import`` in your + own. + +* The **stylesheet** setting gives a list of CSS filenames separated commas which + will be referenced in the HTML header. You can also use CSS' ``@import`` + technique to include one from the other, or use a custom HTML template that + adds ``<link rel="stylesheet">`` tags as necessary. Setting the + :confval:`html_style` config value will override this setting. + +* The **pygments_style** setting gives the name of a Pygments style to use for + highlighting. This can be overridden by the user in the + :confval:`pygments_style` config value. + +* The **pygments_dark_style** setting gives the name of a Pygments style to use + for highlighting when the CSS media query ``(prefers-color-scheme: dark)`` + evaluates to true. It is injected into the page using + :meth:`~sphinx.application.Sphinx.add_css_file()`. + +* The **sidebars** setting gives the comma separated list of sidebar templates + for constructing sidebars. This can be overridden by the user in the + :confval:`html_sidebars` config value. + +* The **options** section contains pairs of variable names and default values. + These options can be overridden by the user in :confval:`html_theme_options` + and are accessible from all templates as ``theme_<name>``. + +.. versionadded:: 1.7 + sidebar settings + +.. versionchanged:: 5.1 + + The stylesheet setting accepts multiple CSS filenames + +.. _distribute-your-theme: + +Distribute your theme as a Python package +----------------------------------------- + +As a way to distribute your theme, you can use a Python package. This makes it +easier for users to set up your theme. + +To distribute your theme as a Python package, please define an entry point +called ``sphinx.html_themes`` in your ``setup.py`` file, and write a ``setup()`` +function to register your themes using ``add_html_theme()`` API in it:: + + # 'setup.py' + setup( + ... + entry_points = { + 'sphinx.html_themes': [ + 'name_of_theme = your_package', + ] + }, + ... + ) + + # 'your_package.py' + from os import path + + def setup(app): + app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__))) + +If your theme package contains two or more themes, please call +``add_html_theme()`` twice or more. + +.. versionadded:: 1.2 + 'sphinx_themes' entry_points feature. + +.. deprecated:: 1.6 + ``sphinx_themes`` entry_points has been deprecated. + +.. versionadded:: 1.6 + ``sphinx.html_themes`` entry_points feature. + + +Templating +---------- + +The :doc:`guide to templating <templating>` is helpful if you want to write your +own templates. What is important to keep in mind is the order in which Sphinx +searches for templates: + +* First, in the user's ``templates_path`` directories. +* Then, in the selected theme. +* Then, in its base theme, its base's base theme, etc. + +When extending a template in the base theme with the same name, use the theme +name as an explicit directory: ``{% extends "basic/layout.html" %}``. From a +user ``templates_path`` template, you can still use the "exclamation mark" +syntax as described in the templating document. + + +.. _theming-static-templates: + +Static templates +~~~~~~~~~~~~~~~~ + +Since theme options are meant for the user to configure a theme more easily, +without having to write a custom stylesheet, it is necessary to be able to +template static files as well as HTML files. Therefore, Sphinx supports +so-called "static templates", like this: + +If the name of a file in the ``static/`` directory of a theme (or in the user's +static path, for that matter) ends with ``_t``, it will be processed by the +template engine. The ``_t`` will be left from the final file name. For +example, the *classic* theme has a file ``static/classic.css_t`` which uses +templating to put the color options into the stylesheet. When a documentation +project is built with the classic theme, the output directory will contain a +``_static/classic.css`` file where all template tags have been processed. + + +Use custom page metadata in HTML templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any key / value pairs in :doc:`field lists </usage/restructuredtext/field-lists>` +that are placed *before* the page's title will be available to the Jinja +template when building the page within the :data:`meta` attribute. For example, +if a page had the following text before its first title: + +.. code-block:: rst + + :mykey: My value + + My first title + -------------- + +Then it could be accessed within a Jinja template like so: + +.. code-block:: jinja + + {%- if meta is mapping %} + {{ meta.get("mykey") }} + {%- endif %} + +Note the check that ``meta`` is a dictionary ("mapping" in Jinja +terminology) to ensure that using it in this way is valid. + + +Defining custom template functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it is useful to define your own function in Python that you wish to +then use in a template. For example, if you'd like to insert a template value +with logic that depends on the user's configuration in the project, or if you'd +like to include non-trivial checks and provide friendly error messages for +incorrect configuration in the template. + +To define your own template function, you'll need to define two functions +inside your module: + +* A **page context event handler** (or **registration**) function. This is + connected to the :class:`.Sphinx` application via an event callback. +* A **template function** that you will use in your Jinja template. + +First, define the registration function, which accepts the arguments for +:event:`html-page-context`. + +Within the registration function, define the template function that you'd like to +use within Jinja. The template function should return a string or Python objects +(lists, dictionaries) with strings inside that Jinja uses in the templating process + +.. note:: + + The template function will have access to all of the variables that + are passed to the registration function. + +At the end of the registration function, add the template function to the +Sphinx application's context with ``context['template_func'] = template_func``. + +Finally, in your extension's ``setup()`` function, add your registration +function as a callback for :event:`html-page-context`. + +.. code-block:: python + + # The registration function + def setup_my_func(app, pagename, templatename, context, doctree): + # The template function + def my_func(mystring): + return "Your string is %s" % mystring + # Add it to the page's context + context['my_func'] = my_func + + # Your extension's setup function + def setup(app): + app.connect("html-page-context", setup_my_func) + +Now, you will have access to this function in jinja like so: + +.. code-block:: jinja + + <div> + {{ my_func("some string") }} + </div> + + +Add your own static files to the build assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, Sphinx copies static files on the ``static/`` directory of the template +directory. However, if your package needs to place static files outside of the +``static/`` directory for some reasons, you need to copy them to the ``_static/`` +directory of HTML outputs manually at the build via an event hook. Here is an +example of code to accomplish this: + +.. code-block:: python + + from os import path + from sphinx.util.fileutil import copy_asset_file + + def copy_custom_files(app, exc): + if app.builder.format == 'html' and not exc: + staticdir = path.join(app.builder.outdir, '_static') + copy_asset_file('path/to/myextension/_static/myjsfile.js', staticdir) + + def setup(app): + app.connect('build-finished', copy_custom_files) + + +Inject JavaScript based on user configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your extension makes use of JavaScript, it can be useful to allow users +to control its behavior using their Sphinx configuration. However, this can +be difficult to do if your JavaScript comes in the form of a static library +(which will not be built with Jinja). + +There are two ways to inject variables into the JavaScript space based on user +configuration. + +First, you may append ``_t`` to the end of any static files included with your +extension. This will cause Sphinx to process these files with the templating +engine, allowing you to embed variables and control behavior. + +For example, the following JavaScript structure: + +.. code-block:: none + + mymodule/ + ├── _static + │ └── myjsfile.js_t + └── mymodule.py + +Will result in the following static file placed in your HTML's build output: + +.. code-block:: none + + _build/ + └── html + └── _static + └── myjsfile.js + +See :ref:`theming-static-templates` for more information. + +Second, you may use the :meth:`.Sphinx.add_js_file` method without pointing it +to a file. Normally, this method is used to insert a new JavaScript file +into your site. However, if you do *not* pass a file path, but instead pass +a string to the "body" argument, then this text will be inserted as JavaScript +into your site's head. This allows you to insert variables into your project's +JavaScript from Python. + +For example, the following code will read in a user-configured value and then +insert this value as a JavaScript variable, which your extension's JavaScript +code may use: + +.. code-block:: python + + # This function reads in a variable and inserts it into JavaScript + def add_js_variable(app): + # This is a configuration that you've specified for users in `conf.py` + js_variable = app.config['my_javascript_variable'] + js_text = "var my_variable = '%s';" % js_variable + app.add_js_file(None, body=js_text) + # We connect this function to the step after the builder is initialized + def setup(app): + # Tell Sphinx about this configuration variable + app.add_config_value('my_javascript_variable', 0, 'html') + # Run the function after the builder is initialized + app.connect('builder-inited', add_js_variable) + +As a result, in your theme you can use code that depends on the presence of +this variable. Users can control the variable's value by defining it in their +:file:`conf.py` file. + + +.. [1] It is not an executable Python file, as opposed to :file:`conf.py`, + because that would pose an unnecessary security risk if themes are + shared. diff --git a/doc/development/tutorials/autodoc_ext.rst b/doc/development/tutorials/autodoc_ext.rst new file mode 100644 index 0000000..cfd23e7 --- /dev/null +++ b/doc/development/tutorials/autodoc_ext.rst @@ -0,0 +1,141 @@ +.. _autodoc_ext_tutorial: + +Developing autodoc extension for IntEnum +======================================== + +The objective of this tutorial is to create an extension that adds +support for new type for autodoc. This autodoc extension will format +the ``IntEnum`` class from Python standard library. (module ``enum``) + +Overview +-------- + +We want the extension that will create auto-documentation for IntEnum. +``IntEnum`` is the integer enum class from standard library ``enum`` module. + +Currently this class has no special auto documentation behavior. + +We want to add following to autodoc: + +* A new ``autointenum`` directive that will document the ``IntEnum`` class. +* The generated documentation will have all the enum possible values + with names. +* The ``autointenum`` directive will have an option ``:hex:`` which will + cause the integers be printed in hexadecimal form. + + +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:`autodoc_intenum.py`. +The :file:`my_enums.py` will contain the sample enums we will document. + +Here is an example of the folder structure you might obtain: + +.. code-block:: text + + └── source + ├── _ext + │ └── autodoc_intenum.py + ├── conf.py + ├── index.rst + └── my_enums.py + + +Writing the extension +--------------------- + +Start with ``setup`` function for the extension. + +.. literalinclude:: examples/autodoc_intenum.py + :language: python + :linenos: + :pyobject: setup + + +The :meth:`~sphinx.application.Sphinx.setup_extension` method will pull the +autodoc extension because our new extension depends on autodoc. +:meth:`~sphinx.application.Sphinx.add_autodocumenter` is the method that +registers our new auto documenter class. + +We want to import certain objects from the autodoc extension: + +.. literalinclude:: examples/autodoc_intenum.py + :language: python + :linenos: + :lines: 1-7 + + +There are several different documenter classes such as ``MethodDocumenter`` +or ``AttributeDocumenter`` available in the autodoc extension but +our new class is the subclass of ``ClassDocumenter`` which a +documenter class used by autodoc to document classes. + +This is the definition of our new the auto-documenter class: + +.. literalinclude:: examples/autodoc_intenum.py + :language: python + :linenos: + :pyobject: IntEnumDocumenter + + +Important attributes of the new class: + +**objtype** + This attribute determines the ``auto`` directive name. In + this case the auto directive will be ``autointenum``. + +**directivetype** + This attribute sets the generated directive name. In + this example the generated directive will be ``.. :py:class::``. + +**priority** + the larger the number the higher is the priority. We want our + documenter be higher priority than the parent. + +**option_spec** + option specifications. We copy the parent class options and + add a new option *hex*. + + +Overridden members: + +**can_document_member** + This member is important to override. It should + return *True* when the passed object can be documented by this class. + +**add_directive_header** + This method generates the directive header. We add + **:final:** directive option. Remember to call **super** or no directive + will be generated. + +**add_content** + This method generates the body of the class documentation. + After calling the super method we generate lines for enum description. + + +Using the extension +------------------- + +You can now use the new autodoc directive to document any ``IntEnum``. + +For example, you have the following ``IntEnum``: + +.. code-block:: python + :caption: my_enums.py + + class Colors(IntEnum): + """Colors enumerator""" + NONE = 0 + RED = 1 + GREEN = 2 + BLUE = 3 + + +This will be the documentation file with auto-documentation directive: + +.. code-block:: rst + :caption: index.rst + + .. autointenum:: my_enums.Colors diff --git a/doc/development/tutorials/examples/README.rst b/doc/development/tutorials/examples/README.rst new file mode 100644 index 0000000..2b9c01b --- /dev/null +++ b/doc/development/tutorials/examples/README.rst @@ -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. diff --git a/doc/development/tutorials/examples/autodoc_intenum.py b/doc/development/tutorials/examples/autodoc_intenum.py new file mode 100644 index 0000000..75fa204 --- /dev/null +++ b/doc/development/tutorials/examples/autodoc_intenum.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import TYPE_CHECKING, Any + +from sphinx.ext.autodoc import ClassDocumenter, bool_option + +if TYPE_CHECKING: + from docutils.statemachine import StringList + + from sphinx.application import Sphinx + + +class IntEnumDocumenter(ClassDocumenter): + objtype = 'intenum' + directivetype = ClassDocumenter.objtype + priority = 10 + ClassDocumenter.priority + option_spec = dict(ClassDocumenter.option_spec) + option_spec['hex'] = bool_option + + @classmethod + def can_document_member(cls, + member: Any, membername: str, + isattr: bool, parent: Any) -> bool: + try: + return issubclass(member, IntEnum) + except TypeError: + return False + + def add_directive_header(self, sig: str) -> None: + super().add_directive_header(sig) + self.add_line(' :final:', self.get_sourcename()) + + def add_content(self, + more_content: StringList | None, + no_docstring: bool = False, + ) -> None: + + super().add_content(more_content, no_docstring) + + source_name = self.get_sourcename() + enum_object: IntEnum = self.object + use_hex = self.options.hex + self.add_line('', source_name) + + for the_member_name, enum_member in enum_object.__members__.items(): + the_member_value = enum_member.value + if use_hex: + the_member_value = hex(the_member_value) + + self.add_line( + f"**{the_member_name}**: {the_member_value}", source_name) + self.add_line('', source_name) + + +def setup(app: Sphinx) -> None: + app.setup_extension('sphinx.ext.autodoc') # Require autodoc extension + app.add_autodocumenter(IntEnumDocumenter) diff --git a/doc/development/tutorials/examples/helloworld.py b/doc/development/tutorials/examples/helloworld.py new file mode 100644 index 0000000..d6d81fd --- /dev/null +++ b/doc/development/tutorials/examples/helloworld.py @@ -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, + } diff --git a/doc/development/tutorials/examples/recipe.py b/doc/development/tutorials/examples/recipe.py new file mode 100644 index 0000000..c7ebf2a --- /dev/null +++ b/doc/development/tutorials/examples/recipe.py @@ -0,0 +1,159 @@ +from collections import defaultdict + +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, 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 'contains' 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, _priority 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 f'recipe.{node.arguments[0]}' + + def get_objects(self): + yield from self.data['recipes'] + + 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 = f'recipe.{signature}' + anchor = f'recipe-{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, + } diff --git a/doc/development/tutorials/examples/todo.py b/doc/development/tutorials/examples/todo.py new file mode 100644 index 0000000..15368f4 --- /dev/null +++ b/doc/development/tutorials/examples/todo.py @@ -0,0 +1,135 @@ +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 merge_todos(app, env, docnames, other): + if not hasattr(env, 'todo_all_todos'): + env.todo_all_todos = [] + if hasattr(other, 'todo_all_todos'): + env.todo_all_todos.extend(other.todo_all_todos) + + +def process_todo_nodes(app, doctree, fromdocname): + if not app.config.todo_include_todos: + for node in doctree.findall(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 + + if not hasattr(env, 'todo_all_todos'): + env.todo_all_todos = [] + + for node in doctree.findall(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) + + # 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) + app.connect('env-merge-info', merge_todos) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/doc/development/tutorials/helloworld.rst b/doc/development/tutorials/helloworld.rst new file mode 100644 index 0000000..8940e3d --- /dev/null +++ b/doc/development/tutorials/helloworld.rst @@ -0,0 +1,189 @@ +Developing a "Hello world" extension +==================================== + +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. + +.. warning:: + + For this extension, you will need some basic understanding of docutils_ + and Python. + + +Overview +-------- + +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` + +Here is an example of the folder structure you might obtain: + +.. code-block:: text + + └── source + ├── _ext + │ └── helloworld.py + ├── _static + ├── conf.py + ├── somefolder + ├── index.rst + ├── somefile.rst + └── someotherfile.rst + + +Writing the extension +--------------------- + +Open :file:`helloworld.py` and paste the following code in it: + +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: + +Some essential things are happening in this example, and you will see them for +all directives. + +.. rubric:: The directive class + +Our new directive is declared in the ``HelloWorld`` class. + +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: + :lines: 5-9 + +This class extends the docutils_' ``Directive`` class. All extensions that +create directives should extend this class. + +.. seealso:: + + `The docutils documentation on creating directives <docutils directives_>`_ + +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:: + + `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:: The ``setup`` function + +.. currentmodule:: sphinx.application + +This function is a requirement. We use it to plug our new directive into +Sphinx. + +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: + :lines: 12- + +The simplest thing you can do is to 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: + +.. code-block:: rst + + Some intro text here... + + .. helloworld:: + + 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. + + +Using the extension +------------------- + +The extension has to be declared in your :file:`conf.py` file to make Sphinx +aware of it. 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 + +For example: + +.. code-block:: python + + import os + import sys + + sys.path.append(os.path.abspath("./_ext")) + + extensions = ['helloworld'] + +.. tip:: + + 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``. + +You can now use the extension in a file. For 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... + + +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`. + + +.. _docutils: https://docutils.sourceforge.io/ +.. _docutils directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html +.. _docutils nodes: https://docutils.sourceforge.io/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 diff --git a/doc/development/tutorials/index.rst b/doc/development/tutorials/index.rst new file mode 100644 index 0000000..a7eee48 --- /dev/null +++ b/doc/development/tutorials/index.rst @@ -0,0 +1,17 @@ +.. _extension-tutorials-index: + +Extension tutorials +=================== + +Refer to the following tutorials to get started with extension development. + + +.. toctree:: + :caption: Directive tutorials + :maxdepth: 1 + + helloworld + todo + recipe + autodoc_ext + diff --git a/doc/development/tutorials/recipe.rst b/doc/development/tutorials/recipe.rst new file mode 100644 index 0000000..1ed428a --- /dev/null +++ b/doc/development/tutorials/recipe.rst @@ -0,0 +1,227 @@ +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: + :pyobject: RecipeDirective + +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_target_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: + :pyobject: IngredientIndex + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :pyobject: RecipeIndex + +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. + +These index pages can be referenced with the :rst:role:`ref` role by combining +the domain name and the index ``name`` value. For example, ``RecipeIndex`` can be +referenced with ``:ref:`recipe-recipe``` and ``IngredientIndex`` can be referenced +with ``:ref:`recipe-ingredient```. + +.. 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: + :pyobject: RecipeDomain + +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 ``recipe_ingredients``. Each contains 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: + :pyobject: setup + +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: https://docutils.sourceforge.io/docs/ diff --git a/doc/development/tutorials/todo.rst b/doc/development/tutorials/todo.rst new file mode 100644 index 0000000..f23d8ad --- /dev/null +++ b/doc/development/tutorials/todo.rst @@ -0,0 +1,367 @@ +Developing a "TODO" extension +============================= + +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. + + +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 :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`. + +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 :doc:`helloworld`, +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 :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: 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`. + + +.. _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 |