Documenter un projet Python avec Sphinx

Je vous avais montré dans le précédent article comment créer une documentation avec Sphinx. Je vais cette fois-ci vous expliquer comment l'utiliser pour documenter le code d'un projet Python. Et comme Sphinx a été créé à l'origine pour rédiger la documentation officielle du langage Python, vous verrez qu'il contient tout ce qu'il faut pour travailler avec notre ami à la langue fourchue. 😎️

Je vais dans cet article partir du principe que vous avez installé et configuré Sphinx comme je l'ai expliqué dans l'article d'introduction.

Note

Cet article fait partie d'une série consacrée à l'outil de documentation Sphinx :

  1. Introduction à Sphinx, un outil de documentation puissant
  2. Documenter un projet Python avec Sphinx

Cet autre article peut également être intéressant en complément :

Configuration de Sphinx

Tel qu'on l'a mis en place dans le précédent article, Sphinx n'est pas configuré pour permettre la documentation de code Python. Heureusement, ça se fait très facilement : on n'a rien de nouveau à installer, seulement une extension à activer.

Ouvrez donc le fichier de configuration de Sphinx qui devrait se trouver dans "doc/conf.py", et ajoutons-y l'extension "sphinx.ext.autodoc". Dans mon cas je me retrouve donc avec quelque chose comme ça :

# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.githubpages',
    'sphinx.ext.autodoc',  # <== Notre nouvelle extension
]

Et c'est tout, on est prêts à documenter du Python ! 💪️

Documenter une fonction

On va commencer par un truc simple : documenter une fonction. Mais pour ça, il nous faut... une fonction. Je vais donc créer le fichier "fourchelang.py" à la racine du dépôt avec le contenu suivant :

import re

def translate(text):
    """Translate the given text to Parseltongue.

    :param str text: The text to translate.

    :returns: the translated text.
    :rtype: str
    """
    return re.sub(r"[\w\d]", "s", text)

Comme vous pouvez le voir, on va utiliser des docstrings avec un formatage particulier pour décrire le fonctionnement de notre fonction. Je vous détaille ci-dessous la syntaxe avec les principaux éléments que l'on peut utiliser :

"""<Courte explication de ce que fait la fonction (une ou deux lignes max)
suivie d'un saut de ligne>.

<Explications plus longues si besoin... On peut faire plusieurs paragraphes
et utiliser tout le formatage reStructuredText proposé par Sphinx !>.

:param <TYPE> <NOM>: <Description du paramètre>.
:param <NOM>: <Description d'un autre paramètre. Ici on ne précise pas le type, c'est optionnel>.

:returns: <Description de ce qui est retourné (si la fonction retourne quelque chose)>.
:rtype: <Type de ce qui est retourné>

:raises <Exception>: <Description de l'exception>.

"""

Maintenant que l'on a une fonction documentée, voyons comment l'ajouter dans notre doc Sphinx.

Pour commencer je vais créer un fichier fourchelang.rst dans lequel je vais documenter ma fonction (vous pouvez le nommer comme vous voulez, cela n'a aucune importance). Voici son contenu :

Fourchelang
===========

.. autofunction:: fourchelang.translate

Ici on utilise l'instruction "autofunction" fournie par le plugin autodoc, en lui précisant le chemin d'accès à la fonction. Il s'agit du même chemin que l'on utiliserait pour importer la fonction dans du code Python.

Une fois ce fichier créé, il reste à le rajouter à la table des matières dans "index.rst", sinon Sphinx ne pourra pas le faire apparaitre dans la documentation générée :

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   ./hello.rst
   ./fourchelang.rst

On est tout bon, il n'y a plus qu'à regénérer la doc avec la commande suivante :

$ make html

Et là... Oops ! Ça ne marche pas du tout ! 😫️

WARNING: autodoc: failed to import function 'translate' from module 'fourchelang';
the following exception was raised:
No module named 'fourchelang'

En fait, le plugin autodoc essaye d'importer notre fichier mais n'y arrive pas, car notre module Python n'est ni installé, ni dans le "PYTHONPATH"... On va corriger ça tout de suite en rajoutant le chemin de notre dossier dans le path :

$ export PYTHONPATH=.

Maintenant on peut relancer notre commande précédente :

$ make html

Et cette fois-ci, ça fonctionne ! Il ne reste plus qu'à ouvrir le fichier "build/html/fourchelang.html" avec son navigateur favori pour admirer le résultat ! 😁️

Capture d'écran du résultat

Documenter une classe

Maintenant qu'on sait documenter une fonction, documentons un objet un peu plus complexe, comme une classe par exemple. Voici un exemple de classe que j'ai ajouté à "fourchelang.py" :

class Snake:

    """This is a snake.

    :param str name: The snake's name.
    """

    def __init__(self, name):
        """The constructor."""
        self.name = name

    def move(self, x, y):
        """Moves the snake to given position.

        .. WARNING::

            Be careful to not tie knots when moving the snake!

        :param int x: The x position where move to.
        :param int y: The y position where move to.
        """
        pass

    def speak(self):
        """Makes the snake speak."""
        return translate("Hello, I'm a ssssssnake!")

Finalement, il n'y a rien de bien nouveau : on a documenté chaque méthode de la classe comme on l'avait fait précédemment avec la fonction.

Le seul point sur lequel je voudrais attirer votre attention, c'est sur le fait que j'ai documenté les paramètres du constructeur (__init__) directement dans la docstring de la classe. C'est la manière dont ça fonctionne par défaut avec autodoc, mais ce comportement est réglable via une option si vous préférez documenter dans le constructeur (plus d'info par là).

Maintenant qu'on a une classe, on peut ajouter la ligne suivante à "fourchelang.rst" pour voir apparaitre notre classe dans la doc Sphinx :

.. autoclass:: fourchelang.Snake

On regénère la doc et...

Capture d'écran de notre classe... Mais il manque les méthodes !

... et... Elles sont passées où les méthodes de la classe ?! 😾️

En fait, par défaut Sphinx ne va afficher que la documentation de la classe elle-même. Il va falloir lui dire que l'on veut les méthodes aussi. Pour ce faire on a deux choix.

Soit on lui liste toutes les méthodes que l'on souhaite afficher (pratique si on ne veut pas toutes les voir apparaitre dans notre doc) :

.. autoclass:: fourchelang.Snake
   :members: move, speak

Soit on ne lui donne pas la liste, et il les affichera toutes automatiquement :

.. autoclass:: fourchelang.Snake
   :members:

On rebuild la doc, et cette fois c'est bon, on a bien tout ! 😤️

Capture d'écran de notre classe avec toutes les méthodes documentées

Documenter une variable ou un attribut

Il y a des éléments, comme les variables ou les attributs d'une classe, que l'on peut vouloir documenter... Le problème c'est que l'on ne peut pas y associer une docstring. Heureusement, on peut utiliser des commentaires avec une syntaxe particulière pour les documenter :

#: The name of the snake
SNAKE_NAME = "SssneakyZeSssnake"

Comme vous pouvez le voir ci-dessus, il suffit de faire suivre le symbole de commentaire (#) par un double point (:).

Une fois le commentaire ajouté, il suffit d'utiliser l'instruction "autodata" pour faire apparaitre cet élément dans notre documentation :

.. autodata:: fourchelang.SNAKE_NAME

Voici le résultat une fois la doc régénérée :

Capture d'écran de notre variable documentée

Documenter un module

Les modules (les fichiers) Python aussi peuvent être documentés en utilisant des docstrings. Voici un exemple de notre module "fourchelang.py" avec une docstring ainsi que tous les éléments vus précédemment histoire d'avoir un exemple complet :

"""Fourchelang library for snakes lovers!"""


import re


#: The name of the snake
SNAKE_NAME = "SssneakyZeSssnake"


def translate(text):
    """Translate the given text to Parseltongue.

    :param str text: The text to translate.

    :returns: the translated text.
    :rtype: str
    """
    return re.sub(r"[\w\d]", "s", text)


class Snake:

    """This is a snake.

    :param str name: The snake's name.
    """

    def __init__(self, name):
        """The constructor."""
        self.name = name

    def move(self, x, y):
        """Moves the snake to given position.

        .. WARNING::

            Be careful to not tie knots when moving the snake!

        :param int x: The x position where move to.
        :param int y: The y position where move to.
        """
        pass

    def speak(self):
        """Makes the snake speak."""
        return translate("Hello, I'm a ssssssnake!")

Ici, c'est """Fourchelang library for snakes lovers!""" la docstring du module. On peut bien sûr la faire beaucoup plus longue, avec plein de paragraphes et tout ! 😉️

Et voici le fichier "fourchelang.rst" avec le module documenté à l'aide de l'instruction "automodule", et contenant également tout ce que l'on a vu précédemment :

Fourchelang
===========

.. automodule:: fourchelang

.. autofunction:: fourchelang.translate

.. autoclass:: fourchelang.Snake
   :members:

.. autodata:: fourchelang.SNAKE_NAME

Si on génère notre documentation, cela nous donnera ce résultat :

Capture d'écran de notre module documenté

Pas mal hein ? 😁️

Mais...

On peut faire plus court !

Comme pour les classes, on peut documenter un module Python entier avec une seule instruction. On peut donc remplacer tout le contenu du fichier "fourchelang.rst" par simplement ceci :

Fourchelang v2
==============

.. automodule:: fourchelang
   :members:

Résultat :

Capture d'écran de notre module documenté

L'ordre des éléments et un peu différent, mais le résultat est similaire.

On peut donc soit documenter les éléments d'un module un par un, si on veut rajouter des titres et des explications supplémentaires au milieu par exemple, soit laisser Sphinx générer tout le contenu du fichier.

Ajouter des exemples avec Doctest

Lorsque l'on écrit une documentation, il est préférable d'y ajouter des exemples pour en faciliter la compréhension. On peut bien sûr le faire en utilisant l'instruction "code-block" de reStructuredText, mais il existe également une autre possibilité : Doctest.

Avec Doctest, l'exemple se présente exactement comme si on le tapait dans l'interpréteur Python, et ce qui est particulièrement intéressant, c'est que les exemples écrits dans ce format peuvent être testés ! On pourra donc s'assurer que les exemples fonctionnent, et qu'ils continuent de fonctionner avec le temps.

Reprenons notre fonction translate() de tout à l'heure. Voici comment lui ajouter des exemples avec Doctest :

def translate(text):
    """Translate the given text to Parseltongue.

    :param str text: The text to translate.

    :returns: the translated text.
    :rtype: str

    >>> translate("Hello!")
    'sssss!'
    >>> translate("I am a sssnake!")
    's ss s sssssss!'
    """
    return re.sub(r"[\w\d]", "s", text)

Si on regénère la doc, ça nous donnera ceci :

Capture d'écran de la fonction documentée avec un exemple Doctest

Et comme je vous l'ai dit, il est possible de tester cet exemple. Cela se fait à l'aide de la commande suivante :

$ python -m doctest -v fourchelang.py
Trying:
    translate("Hello!")
Expecting:
    'sssss!'
ok
Trying:
    translate("I am a sssnake!")
Expecting:
    's ss s sssssss!'
ok

Je ne vous en dis pas plus sur Doctest, j'ai déjà écrit un article complet sur le sujet ! 😉️

Sss sssssss sss ssss sssssss !

Cet article est déjà terminé ! Je vous mets ci-dessous quelques liens utiles, ainsi qu'un zip contenant tout le code vu dans l'article :

À bientôt ! 😁️