15  Bien documenter son code

La documentation est souvent reléguée au second plan dans le développement logiciel, considérée comme un mal nécessaire ou une tâche secondaire. Pourtant, un code bien écrit ne se suffit pas toujours à lui-même, surtout lorsqu’il est utilisé ou maintenu par d’autres personnes… ou soi-même quelques mois plus tard.

Ce chapitre explore pourquoi et comment bien documenter son code : quand ajouter des commentaires, comment structurer des docstrings utiles, quelles conventions adopter, et quels outils utiliser. L’objectif n’est pas de tout documenter, mais de documenter intelligemment.

Parfois, pour expliquer un code complexe, il peut être nécessaire de documenter certains passages et d’ajouter un commentaire explicatif. Il y a cependant une énorme différence entre les commentaires associés au code et la documentation du code :

Pour que le code soit utilisé correctement, il est dès lors important de documenter :

Les commentaires peuvent être utiles lorsque le code n’est pas évident à lire ou lorsque certaines optimisations auraient été appliquées dans un souci de performances. Dans la majorité des cas, si des commentaires doivent être rédigés pour que le code devienne lisible, c’est que ce code n’est pas correctement écrit. Et à moins d’avoir une hygiène de développement aux petits oignons - c’est-à-dire à suivre toutes les bonnes pratiques pour conserver un code lisible, fiable, maintenable et collaboratif -, une décorrélation entre le code et ses commentaires peut souvent être constatée :

# Check that the maximum length of the array if less than 8
if len(array) < 10:
    ...

Il faut trouver une juste mesure entre “tout documenter” et “bien documenter”. Ce n’est pas la quantité qui importe, mais la pertinence. Il est ainsi inutile :

Tip

En résumé, vous pouvez être obsédé par la documentation, mais le code reste la référence.

Pour la blague, même Microsoft s’en sort elle-même parfois difficilement, notamment avec la documentation de la fonction NdrClientCall3 (rpcndr.h) :

Est-ce qu’il fallait réellement passer du temps à documenter une fonction de cette manière-là ? 😋

15.1 Standard de documentation (PEP 257)

Il existe plusieurs types de balisages reconnus :

  • RestructuredText
  • Numpy
  • Google Style (parfois connue sous l’intitulé Napoleon)

… mais tout système de balisage peut être reconnu, sous réseve de respecter la structure de la PEP257, qui donne des recommandations haut-niveau concernant la structure des docstrings : ce qu’elles doivent contenir et comment l’expliciter, sans imposer quelle que mise en forme de contenu que ce soit.

“A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn’t do is insist that you follow it against your will. That’s Python!”

– Tim Peters on comp.lang.python, 2001-06-16

Les conventions décrivent ainsi la manière dont le formatage doit être utilisé, tandis que chaque format propose ensuite son propre balisage, à condition que les sections sont définies selon les types ci-dessous :

  • Une courte description (de la classe, du module, de la fonction, …). Chaque module devrait avoir une docstring au tout début du fichier. Cette docstring peut s’étendre sur plusieurs lignes (Short summary)
  • Des avertissements liés à la dépréciation: quand la fonctionnalité viendra à disparaitre (dans quelle version), les raisons de sa dépréciation, et les recommandations pour arriver au même résultat. (Deprecated)
  • Une description plus précise des fonctionnalités proposées (et pas de la manière dont l’implémentation est réalisée). (Extended summary)
  • Les paramètres attendus, leur type et une description (Parameters)
  • La ou les valeurs de retour, accompagnées de leur type et d’une description (Returns)
  • Les valeurs générées (yields) dans le cas de générateurs (Yields)
  • Un objet attendu par la méthode send() (toujours dans le cas de générateurs) (Receives)
  • D’autres paramètres facultatifs, notamment dans le cas des paramètres *args et **kwargs. (Other parameters)
  • Les exceptions levées (Raises)
  • Les avertissements destinés à l’utilisateur (Warnings)
  • Une section “Pour continuer… (See also)
  • Des notes complémentaires (Notes)
  • Des références (References)
  • Des exemples (Examples)
Tip
  • Évitez de décrire l’évidence.
  • Commencez toujours par une phrase résumant l’objectif.
  • Précisez les paramètres et les exceptions levées.
  • Restez synchronisé avec le code : refactor = mise à jour de la doc.

15.2 Styles de docstrings

Bien qu’il existe plusieurs styles de documentation en Python (ReStructuredText, Numpty), celle qui nous semble la plus lisible reste Napoleon, qui sont les conventions proposées par Google. Elles sont parfois moins bien intégrées que les docstrings officielles (par exemple, avec clize ou la documentation Django automatique qui ne reconnaissent que du ReStructuredText, mais elles restent très lisibles et facilement maintenables.

Le tout est de voir comment votre équipe fonctionne, vos besoins, vos prérequis et ce que vous voulez en faire au final.

L’exemple donné dans les guides de style de Google est celui-ci, et on peut remarquer sans trop se tortiller les neurones que tout reste très lisible :

def fetch_smalltable_rows(
    table_handle: smalltable.Table,
    keys: Sequence[Union[bytes, str]],
    require_all_keys: bool = False,
) -> Mapping[bytes, Tuple[str]]:
    """Fetches rows from a Smalltable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by table_handle. String keys will be UTF-8 encoded.

    Args:
        table_handle: An open smalltable.Table instance.
        keys: A sequence of strings representing the key of each table
                row to fetch. String keys will be UTF-8 encoded.
        require_all_keys: Optional; If require_all_keys is True only
                rows with values set for all keys will be returned.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

            {
                b'Serak': ('Rigel VII', 'Preparer'),
                b'Zim': ('Irk', 'Invader'),
                b'Lrrr': ('Omicron Persei 8', 'Emperor')
            }

            Returned keys are always bytes. If a key from the keys argument is
            missing from the dictionary, then that row was not found in the
            table (and require_all_keys must have been False).

        Raises:
            IOError: An error occurred accessing the smalltable.
        """

15.3 Et Swagger dans tout ça ?

  • Teaser sur les APIs REST (vers un autre chapitre)
  • Présentation rapide : Swagger = doc interactive basée sur OpenAPI
  • Quand ça devient utile

15.4 Conclusion

  • “Le code est la source de vérité, mais une bonne documentation en est le guide”
  • Lien possible avec chapitre sur CI/CD, design d’API, clean code…