Skip to content

Commit 481c5e9

Browse files
authored
Merge pull request #9534 from astrojuanlu/new-tutorial-describing-code
New Sphinx tutorial, part III
2 parents c922189 + 4920e4e commit 481c5e9

File tree

12 files changed

+413
-3
lines changed

12 files changed

+413
-3
lines changed
25.9 KB
Loading
70.1 KB
Loading
40.8 KB
Loading
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
Automatic documentation generation from code
2+
============================================
3+
4+
In the :ref:`previous section <tutorial-describing-objects>` of the tutorial
5+
you manually documented a Python function in Sphinx. However, the description
6+
was out of sync with the code itself, since the function signature was not
7+
the same. Besides, it would be nice to reuse `Python
8+
docstrings <https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring>`_
9+
in the documentation, rather than having to write the information in two
10+
places.
11+
12+
Fortunately, :doc:`the autodoc extension </usage/extensions/autodoc>` provides this
13+
functionality.
14+
15+
Reusing signatures and docstrings with autodoc
16+
----------------------------------------------
17+
18+
To use autodoc, first add it to the list of enabled extensions:
19+
20+
.. code-block:: python
21+
:caption: docs/source/conf.py
22+
:emphasize-lines: 4
23+
24+
extensions = [
25+
'sphinx.ext.duration',
26+
'sphinx.ext.doctest',
27+
'sphinx.ext.autodoc',
28+
]
29+
30+
Next, move the content of the ``.. py:function`` directive to the function
31+
docstring in the original Python file, as follows:
32+
33+
.. code-block:: python
34+
:caption: lumache.py
35+
:emphasize-lines: 2-11
36+
37+
def get_random_ingredients(kind=None):
38+
"""
39+
Return a list of random ingredients as strings.
40+
41+
:param kind: Optional "kind" of ingredients.
42+
:type kind: list[str] or None
43+
:raise lumache.InvalidKindError: If the kind is invalid.
44+
:return: The ingredients list.
45+
:rtype: list[str]
46+
47+
"""
48+
return ["shells", "gorgonzola", "parsley"]
49+
50+
Finally, replace the ``.. py:function`` directive from the Sphinx documentation
51+
with :rst:dir:`autofunction`:
52+
53+
.. code-block:: rst
54+
:caption: docs/source/usage.rst
55+
:emphasize-lines: 3
56+
57+
you can use the ``lumache.get_random_ingredients()`` function:
58+
59+
.. autofunction:: lumache.get_random_ingredients
60+
61+
If you now build the HTML documentation, the output will be the same!
62+
With the advantage that it is generated from the code itself.
63+
Sphinx took the reStructuredText from the docstring and included it,
64+
also generating proper cross-references.
65+
66+
You can also autogenerate documentation from other objects. For example, add
67+
the code for the ``InvalidKindError`` exception:
68+
69+
.. code-block:: python
70+
:caption: lumache.py
71+
72+
class InvalidKindError(Exception):
73+
"""Raised if the kind is invalid."""
74+
pass
75+
76+
And replace the ``.. py:exception`` directive with :rst:dir:`autoexception`
77+
as follows:
78+
79+
.. code-block:: rst
80+
:caption: docs/source/usage.rst
81+
:emphasize-lines: 4
82+
83+
or ``"veggies"``. Otherwise, :py:func:`lumache.get_random_ingredients`
84+
will raise an exception.
85+
86+
.. autoexception:: lumache.InvalidKindError
87+
88+
And again, after running ``make html``, the output will be the same as before.
89+
90+
Generating comprehensive API references
91+
---------------------------------------
92+
93+
While using ``sphinx.ext.autodoc`` makes keeping the code and the documentation
94+
in sync much easier, it still requires you to write an ``auto*`` directive
95+
for every object you want to document. Sphinx provides yet another level of
96+
automation: the :doc:`autosummary </usage/extensions/autosummary>` extension.
97+
98+
The :rst:dir:`autosummary` directive generates documents that contain all the
99+
necessary ``autodoc`` directives. To use it, first enable the autosummary
100+
extension:
101+
102+
.. code-block:: python
103+
:caption: docs/source/conf.py
104+
:emphasize-lines: 5
105+
106+
extensions = [
107+
'sphinx.ext.duration',
108+
'sphinx.ext.doctest',
109+
'sphinx.ext.autodoc',
110+
'sphinx.ext.autosummary',
111+
]
112+
113+
Next, create a new ``api.rst`` file with these contents:
114+
115+
.. code-block:: rst
116+
:caption: docs/source/api.rst
117+
118+
API
119+
===
120+
121+
.. autosummary::
122+
:toctree: generated
123+
124+
lumache
125+
126+
Remember to include the new document in the root toctree:
127+
128+
.. code-block:: rst
129+
:caption: docs/source/index.rst
130+
:emphasize-lines: 7
131+
132+
Contents
133+
--------
134+
135+
.. toctree::
136+
137+
usage
138+
api
139+
140+
Finally, after you build the HTML documentation running ``make html``, it will
141+
contain two new pages:
142+
143+
- ``api.html``, corresponding to ``docs/source/api.rst`` and containing a table
144+
with the objects you included in the ``autosummary`` directive (in this case,
145+
only one).
146+
- ``generated/lumache.html``, corresponding to a newly created reST file
147+
``generated/lumache.rst`` and containing a summary of members of the module,
148+
in this case one function and one exception.
149+
150+
.. figure:: /_static/tutorial/lumache-autosummary.png
151+
:width: 80%
152+
:align: center
153+
:alt: Summary page created by autosummary
154+
155+
Summary page created by autosummary
156+
157+
Each of the links in the summary page will take you to the places where you
158+
originally used the corresponding ``autodoc`` directive, in this case in the
159+
``usage.rst`` document.
160+
161+
.. note::
162+
163+
The generated files are based on `Jinja2
164+
templates <https://jinja2docs.readthedocs.io/>`_ that
165+
:ref:`can be customized <autosummary-customizing-templates>`,
166+
but that is out of scope for this tutorial.

doc/tutorial/describing-code.rst

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
Describing code in Sphinx
2+
=========================
3+
4+
In the :doc:`previous sections of the tutorial </tutorial/index>` you can read
5+
how to write narrative or prose documentation in Sphinx. In this section you
6+
will describe code objects instead.
7+
8+
Sphinx supports documenting code objects in several languages, namely Python,
9+
C, C++, JavaScript, and reStructuredText. Each of them can be documented using
10+
a series of directives and roles grouped by
11+
:doc:`domain </usage/restructuredtext/domains>`. For the remainder of the
12+
tutorial you will use the Python domain, but all the concepts seen in this
13+
section apply for the other domains as well.
14+
15+
.. _tutorial-describing-objects:
16+
17+
Documenting Python objects
18+
--------------------------
19+
20+
Sphinx offers several roles and directives to document Python objects,
21+
all grouped together in :ref:`the Python domain <python-domain>`. For example,
22+
you can use the :rst:dir:`py:function` directive to document a Python function,
23+
as follows:
24+
25+
.. code-block:: rst
26+
:caption: docs/source/usage.rst
27+
28+
Creating recipes
29+
----------------
30+
31+
To retrieve a list of random ingredients,
32+
you can use the ``lumache.get_random_ingredients()`` function:
33+
34+
.. py:function:: lumache.get_random_ingredients(kind=None)
35+
36+
Return a list of random ingredients as strings.
37+
38+
:param kind: Optional "kind" of ingredients.
39+
:type kind: list[str] or None
40+
:return: The ingredients list.
41+
:rtype: list[str]
42+
43+
Which will render like this:
44+
45+
.. figure:: /_static/tutorial/lumache-py-function.png
46+
:width: 80%
47+
:align: center
48+
:alt: HTML result of documenting a Python function in Sphinx
49+
50+
The rendered result of documenting a Python function in Sphinx
51+
52+
Notice several things:
53+
54+
- Sphinx parsed the argument of the ``.. py:function`` directive and
55+
highlighted the module, the function name, and the parameters appropriately.
56+
- The directive content includes a one-line description of the function,
57+
as well as a :ref:`info field list <info-field-lists>` containing the function
58+
parameter, its expected type, the return value, and the return type.
59+
60+
.. note::
61+
62+
The ``py:`` prefix specifies the :term:`domain`. You may configure the
63+
default domain so you can omit the prefix, either globally using the
64+
:confval:`primary_domain` configuration, or use the
65+
:rst:dir:`default-domain` directive to change it from the point it is called
66+
until the end of the file.
67+
For example, if you set it to ``py`` (the default), you can write
68+
``.. function::`` directly.
69+
70+
Cross-referencing Python objects
71+
--------------------------------
72+
73+
By default, most of these directives generate entities that can be
74+
cross-referenced from any part of the documentation by using
75+
:ref:`a corresponding role <python-roles>`. For the case of functions,
76+
you can use :rst:role:`py:func` for that, as follows:
77+
78+
.. code-block:: rst
79+
:caption: docs/source/usage.rst
80+
81+
The ``kind`` parameter should be either ``"meat"``, ``"fish"``,
82+
or ``"veggies"``. Otherwise, :py:func:`lumache.get_random_ingredients`
83+
will raise an exception.
84+
85+
When generating code documentation, Sphinx will generate a cross-reference automatically just
86+
by using the name of the object, without you having to explicitly use a role
87+
for that. For example, you can describe the custom exception raised by the
88+
function using the :rst:dir:`py:exception` directive:
89+
90+
.. code-block:: rst
91+
:caption: docs/source/usage.rst
92+
93+
.. py:exception:: lumache.InvalidKindError
94+
95+
Raised if the kind is invalid.
96+
97+
Then, add this exception to the original description of the function:
98+
99+
.. code-block:: rst
100+
:caption: docs/source/usage.rst
101+
:emphasize-lines: 7
102+
103+
.. py:function:: lumache.get_random_ingredients(kind=None)
104+
105+
Return a list of random ingredients as strings.
106+
107+
:param kind: Optional "kind" of ingredients.
108+
:type kind: list[str] or None
109+
:raise lumache.InvalidKindError: If the kind is invalid.
110+
:return: The ingredients list.
111+
:rtype: list[str]
112+
113+
And finally, this is how the result would look:
114+
115+
.. figure:: /_static/tutorial/lumache-py-function-full.png
116+
:width: 80%
117+
:align: center
118+
:alt: HTML result of documenting a Python function in Sphinx
119+
with cross-references
120+
121+
HTML result of documenting a Python function in Sphinx with cross-references
122+
123+
Beautiful, isn't it?
124+
125+
Including doctests in your documentation
126+
----------------------------------------
127+
128+
Since you are now describing code from a Python library, it will become useful
129+
to keep both the documentation and the code as synchronized as possible.
130+
One of the ways to do that in Sphinx is to include code snippets in the
131+
documentation, called *doctests*, that are executed when the documentation is
132+
built.
133+
134+
To demonstrate doctests and other Sphinx features covered in this tutorial,
135+
Sphinx will need to be able to import the code. To achieve that, write this
136+
at the beginning of ``conf.py``:
137+
138+
.. code-block:: python
139+
:caption: docs/source/conf.py
140+
:emphasize-lines: 3-5
141+
142+
# If extensions (or modules to document with autodoc) are in another directory,
143+
# add these directories to sys.path here.
144+
import pathlib
145+
import sys
146+
sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix())
147+
148+
.. note::
149+
150+
An alternative to changing the :py:data:`sys.path` variable is to create a
151+
``pyproject.toml`` file and make the code installable,
152+
so it behaves like any other Python library. However, the ``sys.path``
153+
approach is simpler.
154+
155+
Then, before adding doctests to your documentation, enable the
156+
:doc:`doctest </usage/extensions/doctest>` extension in ``conf.py``:
157+
158+
.. code-block:: python
159+
:caption: docs/source/conf.py
160+
:emphasize-lines: 3
161+
162+
extensions = [
163+
'sphinx.ext.duration',
164+
'sphinx.ext.doctest',
165+
]
166+
167+
Next, write a doctest block as follows:
168+
169+
.. code-block:: rst
170+
:caption: docs/source/usage.rst
171+
172+
>>> import lumache
173+
>>> lumache.get_random_ingredients()
174+
['shells', 'gorgonzola', 'parsley']
175+
176+
Doctests include the Python instructions to be run preceded by ``>>>``,
177+
the standard Python interpreter prompt, as well as the expected output
178+
of each instruction. This way, Sphinx can check whether the actual output
179+
matches the expected one.
180+
181+
To observe how a doctest failure looks like (rather than a code error as
182+
above), let's write the return value incorrectly first. Therefore, add a
183+
function ``get_random_ingredients`` like this:
184+
185+
.. code-block:: python
186+
:caption: lumache.py
187+
188+
def get_random_ingredients(kind=None):
189+
return ["eggs", "bacon", "spam"]
190+
191+
You can now run ``make doctest`` to execute the doctests of your documentation.
192+
Initially this will display an error, since the actual code does not behave
193+
as specified:
194+
195+
.. code-block:: console
196+
197+
(.venv) $ make doctest
198+
Running Sphinx v4.2.0
199+
loading pickled environment... done
200+
...
201+
running tests...
202+
203+
Document: usage
204+
---------------
205+
**********************************************************************
206+
File "usage.rst", line 44, in default
207+
Failed example:
208+
lumache.get_random_ingredients()
209+
Expected:
210+
['shells', 'gorgonzola', 'parsley']
211+
Got:
212+
['eggs', 'bacon', 'spam']
213+
**********************************************************************
214+
...
215+
make: *** [Makefile:20: doctest] Error 1
216+
217+
As you can see, doctest reports the expected and the actual results,
218+
for easy examination. It is now time to fix the function:
219+
220+
.. code-block:: python
221+
:caption: lumache.py
222+
:emphasize-lines: 2
223+
224+
def get_random_ingredients(kind=None):
225+
return ["shells", "gorgonzola", "parsley"]
226+
227+
And finally, ``make test`` reports success!
228+
229+
For big projects though, this manual approach can become a bit tedious.
230+
In the next section, you will see :doc:`how to automate the
231+
process </tutorial/automatic-doc-generation>`.

0 commit comments

Comments
 (0)