Documenter une appli Django

En plein travail sur un projet

J'ai parlé de Documentation Driven Development l'autre jour. Cela dit, si tout le monde est d'accord pour dire qu'une bonne doc est important, personne ne nous dit jamais ce qu'est vraiment une « bonne » doc.

Du coup, on se retrouve souvent avec des docs pourries et donc des logiciels difficiles / pénibles à utiliser.

Voyons donc comment documenter correctement une application Django (ou n'importe quel projet Python, hein, c'est juste pour l'exemple concret et mon référencement).

Cas d'école avec une application Django

Mon pet project envoie des sms. Ayant déjà rencontré un besoin similaire, j'avais écrit django-nexmo, une application qui utilisait le provider Nexmo.com pour envoyer des sms depuis un projet Django. Cool !

Ledit code étant obsolète et nécessitant de gros refactorings, j'ai décidé de repartir sur du neuf, parce c'est un projet personnel et que je ne suis pas là pour m'e**der.

Sachant que le besoin n'est pas « envoyer des sms avec Nexmo » mais simplement « envoyer des sms », j'ai décidé d'écrire django-simple-sms, une appli qui envoie du bois et… des sms, et reste agnostique sur le provider utilisé en bout de ligne.

Pour m'amuser, j'ai essayé d'écrire intégralement la documentation avant de me lancer dans le code. Les pythonistas ont beaucoup de chance, nous bénéficions d'outils extraordinaires pour écrire et publier de la doc : Sphinx, ReadTheDocs … Voyons comment mettre tout ça en pratique.

1) Initialiser l'application

Première étape : initialiser un squelette d'application avec setup.py, Manifest.in et surtout, un fichier Readme.

Le but du fichier Readme étant de fournir à tout curieux les informations primordiales en un coup d'œil, un bon fichier Readme doit contenir les éléments suivants :

  • Une description en un mot de l'appli, à quoi sert-elle ?
  • Un lien vers la doc (!)
  • Les infos de compatibilité (versions de Python et Django, dépendances)
  • Rappel des grandes fonctionnalités principales
  • Procédure d'installation si pas plus de quelques lignes
  • Un exemple d'utilisation basique prêt à copier-coller
  • Les infos de contact / contributeurs
  • La licence utilisée
  • J'en oublie ?

2) Créer un dépôt Sphinx

Sphinx est un outil permettant d'écrire et de publier de la documentation sous divers formats (html, pdf, etc.)

À la racine de votre projet, créez un répertoire docs, dans lequel vous lancerez un joyeux:

pip install sphinx
sphinx-quickstart

S'ensuit alors une série de questions / réponses auxquelles il vous suffit de répondre le plus intelligemment possible. Assurez vous de bien confirmer que vous souhaitez activer le module « autodoc: automatically insert docstrings from modules ».

Pour que Sphinx puisse accéder au module de votre application, éditez le fichier conf.py et ajoutez les lignes suivantes (juste après les imports) :

sys.path.insert(0, os.path.abspath('..'))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djsms.test_settings')
from djsms import test_settings as settings

En prenant garde à bien remplacer djsms par votre propre module of course…

Dans son immense bonté, Sphinx génère un Makefile, qui vous permet de bâtir la doc d'un simple make html. La documentation sera alors disponible dans le sous-répertoire _build/html.

3) Compléter la page d'accueil

Pour éditer la page d'accueil de votre doc, éditez le fichier index.rst. Ce sera le point d'entrée, pas la peine à mon avis de la bourrer à craquer ; il me semble plus pertinent de se cantonner à reprendre grosso-modo les éléments du Readme. Le strict minimal me semblant être :

  • qu'est-ce que c'est ? (une appli Django)
  • à quoi ça sert ? (à envoyer des sms)
  • comment je peux en savoir plus ? (voici la table des matières)

Pour intégrer des liens vers d'autres pages, vous pouvez intégrer quelque chose qui ressemble au code suivant dans le fichier susmentionné.

Contents
--------

.. toctree::
   :maxdepth: 2

   installation
   sending
   status_report
   incoming
   backends
   exceptions
   colophon

Il vous faudra alors compléter les pages correspondantes : installation.rst, colophon.rst, etc.

4) Décider de ce qu'on met dans la doc

C'est pas tout ça, mais cette documentation, qu'est-ce qu'on y met ?

La réponse dépendra évidemment du contexte du projet, du type de logiciel documenté et de la cible à qui l'on souhaite s'adresser. Voici néanmoins quelques pistes de réflexion.

  • Une méthode d'installation détaillée
  • Des exemples d'utilisation prêt à copier-coller qui couvrent les cas d'utilisation basiques / courants
  • Un tutoriel pas à pas si applicable
  • Détail des paramètres de configuration
  • Une doc détaillée de chaque grande feature
  • Une doc détaillée de l'API
  • Reprise des infos de contact, licence, etc.

5) Documenter vos grandes features

Là encore, tout dépend de la cible à qui s'adresse la documentation, mais en général, on s'adresse à des humains (ça ne compte pas si ce sont des sysadmins, je pense qu'ils constituent une espèce à part entière).

Du coup, il me semble intéressant d'organiser la doc en thème plutôt que selon un aspect technique. Regrouper le « quoi » plutôt que le « comment ».

L'appli dont nous parlons regroupe trois grandes fonctionnalités : envoyer des sms, recevoir des sms, recevoir des accusés de réception. Chacune de ces fonctionnalité fait l'objet d'une page cohérente et autant que faire se peut auto-suffisante (exemples, api, considérations techniques et fonctionnelles, etc.).

Une page qui reprend l'ensemble de l'API peut également être assez utile.

6) Maîtriser la syntaxe reStructuredText

reStructuredText (ou rst pour les paresseux) est un format « plein texte » permettant de générer du texte enrichi, un peu à la façon de Markdown. Rst est le format utilisé par Sphinx.

Voici un exemple de document rst. Notez que j'écris mes billets de blog en rst, et qu'en rédigeant le snippet suivant je vis actuellement un grand moment inceptionesque.

Le titre de ma page
===================

Un paragraphe. Si je saute un ligne…

…je créé un nouveau paragraphe.

Un sous titres
--------------

Voici un exemple de code.

.. code:: python

    # -*- coding: utf-8 -*-

    print 'Hello World!'

Je peux également créer un bloc littéral grâce à l'indentation.

    Littéral mais pas limité !

Gras, italique…
---------------

Il est très facile d'écrire du texte en *gras*, **italique** ou
``monospace``.

Listes, etc.
------------

* une
* liste
* non-ordonnée

#. une
#. liste
#. ordonnée

Notes et warning
----------------

.. note::

    Ce paragraphe s'affiche dans un joli cadre bleu.

.. warning::

    Celui-ci dans un cadre orange assez effrayant.

Liens
-----

La syntaxe pour `les liens est assez horrible <http://example.com>`_.

Une `syntaxe alternative`_ est possible.

.. _syntaxe alternative: http://example.com

Je vous laisse consulter la référence syntaxique pour en savoir plus.

7) Utiliser autodoc pour documenter son API

Vous le savez sûrement, la documentation est intégrée au cœur même de la syntaxe du langage Python, grâce aux docstrings.

Ainsi, il est possible d'écrire quelque chose comme…

def send_text(text, frm, to, fail_silently=True, status_report=False):
    """A convenient helper to quickly access the application features."""

Ensuite, dans l'interpréteur :

>>> from djsms import send_text
>>> help(send_text)

Help on function send_text in module djsms.base:

send_text(text, frm, to, fail_silently=True, status_report=False)
    A convenient helper to quickly access the application features.

Sphinx propose l'extension autodoc, qui va nous permettre d'intégrer nos docstrings plus facilement dans la documentation.

Par exemple avec ce code :

def send_text(text, frm, to, fail_silently=True, status_report=False):
    """A convenient helper to quickly access the application features.

    Returns a :class:`~djsms.models.TextMessage` object.

    Required parameters:

    :param text: the text message body. It's length is not limited, but if it's
        too long, your carrier will probably split it and send in a multipart
        text message, and you will be charged accordingly
    :param frm: the originator phone number. Some carriers accept
        alphanumerical values, but it really depends on mobile networks and
        local laws.
    :param to: the recipient phone number.

    Additionals parameters:

    :param fail_silently: If True, errors will just be ignored.
    :param status_report: If True, asks the selected carrier to provide
        status report by asynchronously calling an uri. If your carrier does'nt
        provide this option, the parameter value will simply be ignored.

    Raises:

        Any :class:`~djsms.exceptions.TextMessageError` subclass if
        ``fail_silently`` is False.

    """
    pass

Il me suffit d'inclure cette simple instruction dans ma doc :

.. automodule:: djsms
    :members: send_text

Pour obtenir le résultat suivant (capture d'écran de la véritable doc) :

capture d'écran de la doc de la fonction send_text

Ainsi, les directives rst .. auto*:: permettent automatiquement d'inclure les docstrings de divers éléments de votre code Python. Il est possible de compléter les éléments automatiquement insérés :

.. autoclass:: djsms.models.TextMessage
    :members:

    .. warning::

        The ``price`` attributes contains the price charged by the provider for
        this text message. This price can be expressed in any currency, however,
        depending on your account configuration. Check your provider documentation
        to know more.

    .. data:: STATUSES

        The list of different possible statuses.

            * `created`: the message was just created in the database and was
              not yet submitted to your text message provider
            * `sending`: the message was successfully sent to your provider
            * `sent`: the message was successfully sent to the upstream carrier
            * `delivered`: the message was successfully delivered to the
              destination handset
            * `refused`: the message was refused by your provider
            * `rejected`: the message was refused by the destination carrier

        .. warning::

            Because a message was received and accepted by your text message
            provider does not mean it will be accepted by the destination
            carrier, nor the destination handset. Be careful not to be confused
            by the different statuses.

Vous pouvez vérifier le résultat obtenu sur la doc du projet.

Évidemment, il y a un équilibre à trouver. En multipliant les docstrings, on finit par obtenir quelque chose d'ingérable et qui ressemble un peu trop à du Java.

8) Publier sa doc sur ReadTheDocs.org

ReadTheDocs est un projet fort sympathique qui vous propose d'héberger gracieusement la doc de votre projet écrit avec Sphinx. Vous obtenez alors une url du type mon-projet.readthedocs.org.

Commencez par vous créer un compte puis, dans votre dashboard, cliquez sur « Import a project ». Vous pourrez alors indiquer le dépôt git concerné. C'est encore plus simple si votre projet est hébergé sur Github, vous avez l'option d'importer directement votre projet.

ReadTheDocs vous permet de construire et maintenir plusieurs versions de votre documentation. Exemple : latest (branche master) et stable (dernier tag), etc.

Vous pouvez également configurer Github pour que la doc soit reconstruite à chaque « push ». Dans les settings de votre projet, rendez-vous dans « Webhooks and services ». Cliquez sur « Add service » et sélectionnez « ReadTheDocs ».

9) C'est tout

Je crois qu'on a fait le tour (rapide). Vous n'aurez plus d'excuse pour livrer des projets avec des docs toutes pourries.

Sur ce, à la revoyure.