Greboca  

DLFP - Dépêches  -  Python — partie 10 ― formateur de code, analyse statique

 -  9 juin - 

Cette dépêche est la suite d’une série sur Python initiée en septembre 2019. Après un sommeil cryogénique d’un an et demi, on repart en forme avec d’autres contenus Python à vous proposer : actualité, bonnes pratiques, astuces, témoignages… Elle a été rédigée principalement à deux voix, Oliver et Philippe, qui vous font part de leur expérience sur les fonctions.

Cette dixième partie présente les formateurs de code bien pratiques et les analyseurs de code. 🐍 🐍 🐍

Le logo de Python est entouré de petites icônes symbolisant la variété des domaines où s’applique Python, et, à droite, un joyeux barbu se tient derrière un écran d’ordinateur qui affiche « partie = 10, "Formateurs" \n print(partie) »

Pour rappel, les autres dépêches déjà publiées :

Sommaire

Formateurs de code source

Les formateurs de code source ne dépendent pas des modules utilisés par les projets. Donc nous pouvons les installer avec pip :

python3 -m pip install --progress-bar emoji --user --upgrade black
python3 -m pip install --progress-bar emoji --user --upgrade yapf
python3 -m pip install --progress-bar emoji --user --upgrade autopep8
python3 -m pip install --progress-bar emoji --user --upgrade docformatter

Par la suite, ils seront illustrés avec l’exemple suivant :

$ cat > bateau.py
capitaine = { 'age':   42,       # univers ?
              'nom':  'Grant',
              'pays': 'Royaume-Uni',
            }
navire    = { 'nom':       'Britannia',
              'longueur':   127,# metres
              'tonnage':    5860,
              'lancement': "16 mars 1953"
            }

mission  =  { "commandant" : capitaine , 'bateau' : navire , }
f  =  lambda x:   True if x%9 == 0   else False

black

Le projet black est très récent, son premier commit date de mars 2018. Et pourtant ce formateur de code Python bénéficie d’un succès énorme avec plus de 20 000 étoiles sur GitHub (et une centaine de contributeurs).

Son succès est lié à la quasi-absence de configuration et fonctionne dans le même esprit que gofmt, c’est-à-dire que les développeurs n’ont plus à discuter des règles de codage. C’est toujours black qui a raison et on ne perd plus de temps à négocier les règles, à les rediscuter en revue de code… On se concentre sur son travail : coder sans se prendre la tête à bien indenter. De toutes façons, black va changer l’indentation avec ses propres règles de codage non-négociables : uncompromising Python code formater.

Les deux seuls paramètres sur lesquels on peut encore chipoter :

  • --line-length 88
  • --skip-string-normalization
    (si présent ne remplace pas 'texte' par "texte")

Exemple :

$ black .
reformatted bateau.py
All done! ✨ 🍰 ✨
1 file reformatted.
$ cat bateau.py
capitaine = {"age": 42, "nom": "Grant", "pays": "Royaume-Uni"}  # univers ?
navire = {
    "nom": "Britannia",
    "longueur": 127,  # metres
    "tonnage": 5860,
    "lancement": "16 mars 1953",
}

mission = {"commandant": capitaine, "bateau": navire}
f = lambda x: True if x % 9 == 0 else False

Une discussion a été ouverte sur le fait de passer le code de la lib standard de Python par black, mais, pour l’instant, il y a pas mal d’éléments qui font que ça n’aura pas lieu. Un des arguments principaux est de ne pas surcharger le nombre d’outils nécessaires pour une contribution à Python.

blue

Le projet blue est un dérivé de black avec quelques ajustements sur les points qui sont les plus controversés.

Les différences avec black:

  • utilisation des simples guillemets pour les chaînes de caractère (en dehors de docstring) ;
  • longueur de ligne à 79 caractères ;
  • configuration via plusieurs mécanismes possibles, pyproject.toml, setup.cfg, tox.ini, et .blue.

Il s’utilise à l’identique de black et est disponible sous pypi.org

yapf

Le projet Yet Another Python Formatter est plus vieux que black (premier commit en mars 2015), a moins d’étoiles (9 700) et le même nombre de contributeurs.

L’innovation de yapf réside dans la réutilisation du puissant clang-format. Les règles de sa configuration sont prises en compte pour calculer le score de tel ou tel reformatage et de boucler ainsi afin d’obtenir le meilleur score.

L’idée est superbe, mais en pratique, on passe trop de temps à essayer de peaufiner la configuration sans trop comprendre quel paramètre influe sur telle indentation. Et comme c’est configurable, une personne va passer du temps pour tenter d’améliorer les choses. Et pire, dans de rares circonstances, yapf peut reformater un code source différemment deux fois de suite ! (avec la même configuration)

En fait, le seul paramètre que nous devrions tester c’est --style avec les valeurs actuelles : pep8 (défaut), google, chromium et facebook.

Le résultat à partir du même fichier d’origine quel que soit le paramètre --style :

$ yapf bateau.py
capitaine = {
    'age': 42,  # univers ?
    'nom': 'Grant',
    'pays': 'Royaume-Uni',
}
navire = {
    'nom': 'Britannia',
    'longueur': 127,  # metres
    'tonnage': 5860,
    'lancement': "16 mars 1953"
}

mission = {
    "commandant": capitaine,
    'bateau': navire,
}

autopep8

Le projet autopep8 est encore plus vieux (premier commit en décembre 2010), a encore moins d’étoiles (3 000) et moins de contributeurs (une trentaine).

Ce formateur de code est beaucoup moins agressif que les deux premiers, car il ne reformate pas ce qui est compatible avec les règles PEP8. Cependant quelques corrections sont intéressantes comme le remplacement de f = lambda x: par def f(x):.

Le formateur autopep8 semble avoir --max-line-length comme seule règle de formatage. En fait, sa configuration est différente des deux autres : l’option --ignore permet de désactiver des règles. Les options --aggressive et --experimental sont intéressantes.

Exemple :

$ autopep8 --aggressive --aggressive --aggressive bateau.py
capitaine = {'age': 42,       # univers ?
             'nom': 'Grant',
             'pays': 'Royaume-Uni',
             }
navire = {'nom': 'Britannia',
          'longueur': 127,  # metres
          'tonnage': 5860,
          'lancement': "16 mars 1953"
          }

mission = {"commandant": capitaine, 'bateau': navire, }


def f(x):
    return True if x % 9 == 0 else False

isort

Le projet isort a une ambition plus modeste que les précédents formateurs de code. Il se focalise sur les imports et vous propose de les reformater pour vous simplifier la vie. La première version officielle date de 2013 et le projet est toujours assez actif et dispose de 3900 étoiles sous GitHub.

Le README du projet donne un bon exemple de son action.

Avant :

from my_lib import Object

import os

from my_lib import Object3

from my_lib import Object2

import sys

from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14

import sys

from __future__ import absolute_import

from third_party import lib3

print("Hey")
print("yo")

Après isort :

from __future__ import absolute_import

import os
import sys

from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8,
                         lib9, lib10, lib11, lib12, lib13, lib14, lib15)

from my_lib import Object, Object2, Object3

print("Hey")
print("yo")

Donc concrètement :

  • regroupement des imports par groupe, les __future__ en premier, la lib standard en deuxième, les autres lib par la suite ;
  • regroupement des imports divers d’une bibliothèque sur un seul import ;
  • tri par ordre alphabétique des imports au sein d’un même groupe ;
  • formatage sous forme de bloc équilibré pour lib1 à lib15 par exemple ;
  • on ne touche pas au reste du code.

L’ambition du projet est modeste mais sympathique, et ça fonctionne bien. À voir s’il est essentiel pour vous d’avoir de beaux imports dans vos projets. Perso, je ne l’utiliserais pas sur mes petits projets, mais sur des projets un peu gros, gérés sur un temps long avec une équipe qui bouge, ça peut être une bonne idée.

Bilan

Retour d’Oliver

Personnellement, je regrette que black mette sur une seule ligne un petit dictionnaire que je trouve plus lisible sur plusieurs lignes. Je suis déçu des quatre styles fournis par yapf, et je n’ai pas trouvé une superbe configuration magique. Finalement, sur notre projet c’est le bon vieux vénérable autopep8 qui est utilisé, car il ne change que très peu le code source que nous écrivons.

Sur notre projet on utilise black dans une ancienne version non taguée. L’intégration continue vérifie que le formatage est « standard ». Et contraint tous les contributeurs à utiliser la même version. Et par la même, limite la mise à jour de black. Malgré ce petit désagrément, j’ai configuré l’outil pour formater à chaque sauvegarde. Laisser un outil faire le formatage complet est très confortable. Avec une grosse base de code, j’ai vraiment autre chose à faire qu’aligner des commentaires et des valeurs de dictionnaires.

Retour de Philippe

J’ai testé pour vous les différents formatters sur un petit projet libre. Il s’agit d’un projet développé initialement par un stagiaire, que j’ai retravaillé avant que ma boite ne le mette en open source. Du coup, le style est un peu hétérogène, ça fait un bon candidat.

Voici les liens vers les diff sous GitHub :

Mon point de vue général : sur un projet où je travaille seul, je vais pas utiliser de formateur de code. Je sais assez bien ce que j’aime et je suis relativement cohérent sur mon style. Sur un projet en équipe, c’est à réfléchir, mais je trouve le style de black/blue plutôt désagréable. Ma tentation de l’utiliser viendra donc de l’écart entre mon style et celui de mes coparticipants : s’il est grand, autant unifier avec un outil extérieur. Si on est proche, on garde comme ça.

À noter que j’ai un style pas forcément commun. J’aime bien que l’affichage ait une densité raisonnable. Par exemple, si je peux faire en sorte de voir la totalité d’une fonction sur un seul écran en tirant parti de lignes un peu plus longues, il y a un vrai bénéfice pour moi puisque je peux capturer en un coup d’œil l’ensemble du traitement. C’est pour ça que j’ai réglé la longueur maximum de ligne à 110 (ou 120 quand je me suis gouré) car c’est ce que j’affiche sans problème sur un portable 14 pouces. J’utilise aussi le formatage pour faire ressortir des similitudes dans le code, ce qui va s’opposer parfois à un formatage agressif tenant compte avec rigueur du niveau d’imbrication des structures.

Mon bilan sur ce projet-là :

  • isort, c’est gentil, j’aime bien, mais je vois pas trop l’intérêt. La valeur ajoutée est vraiment très faible, surtout que j’aime bien grouper tous les imports de la stdlib de Python sur une seule ligne (notion de densité de code évoquée plus haut), ce qu’il se refuse à faire (comme la majorité des gens) ;
  • autopep8, c’est assez peu envahissant. Ça me convient bien pour rectifier une base de code comme dans le projet que j’ai pris, sans pour autant tout péter mon style. J’aime bien ;
  • yapf, black, blue : honnêtement, je suis entre deux. Il y a clairement des gains en lisibilité par endroits, et d’autres où le code devient inutilement étalé sur plusieurs lignes et où la perte de densité me semble dommageable à la compréhension globale. Donc je suis réservé sur l’amélioration, mais pas hostile au concept en général.

Finalement, tout ça est vraiment très subjectif. Je comprends tout à fait pourquoi des gros projets ont adopté black, au moins, on évite ce type de discussion et le style reste tout à fait raisonnable.

Formateurs de docstring

docformatter

Le projet docformatter permet de reformater la partie docstring du code source.

Nous l’utilisons avec ces paramètres :

docformatter --wrap-summaries 444 --pre-summary-newline --in-place --recursive .

pyment

La documentation du code source Python se fait à l’aide des docstring standardisée par la PEP 257 (2001). Plusieurs types de docstring sont utilisés, les plus connus étant :

L’outil pyment permet de créer, corriger et de modifier ces représentations docstring.

L’auteur de pyment, Adel Daouzli (dadadel) nous avait présenté son outil dans son journal (2014). Mais Adel ne semble plus maintenir le code source ces derniers temps.

Comme plusieurs bugs sont corrigés dans des Pull Requests fights, j’ai donc pris en compte ces corrections et autres améliorations apportées et j’ai tout *mergé sur la branche olibre publiée sur un triple fork :

Attention, l’annotation des types (type hints Python 3.5) n’est pas prise en charge par pyment.

Génération de la documentation

C’est pratique quand la documentation de son code source et automatiquement générée. Deux outils intéressants :

  • Pdoc, successeur du bon vieux Epydoc ;
  • Sphinx, LE générateur de documentation Python le plus connu.

Attention, pour Pdoc, nous avons deux projets qui ont divergé : pdoc (l’original) et pdoc3 (le fork, plus actif).

Analyse statique de code

pylint

Pylint est je cite : « un outil qui recherche des erreurs dans le code Python, qui essaye d’imposer un standard de codage et qui cherche du code malodorant (code smells). Il peut aussi trouver certains types d’erreurs, faire des recommandations sur la façon dont un bloc peut être réorganisé et détaille la complexité du code ».

Pylint est un projet ancien (plus de 15 ans), qui analyse le code Python dans plusieurs optiques différentes :

  • conformité à un style de codage, le fameux pep8 plus quelques petits détails supplémentaires ;
  • analyse de la complexité du code (nombre de chemins d’exécution dans une fonction, etc.) ;
  • erreurs de codage ;
  • améliorations possibles (suppression de parenthèses, simplifications…).

Chaque problème reporté peut-être désactivable, en ligne de commande ou via une variété de fichiers de configuration (.pylintrc, pyproject.toml ou setup.cfg). On peut aussi dans le code activer ou désactiver spécifiquement des configurations, au niveau du fichier, d’une fonction, d’un bloc de code ou, tout simplement, d’une ligne.

Retour de Philippe

J’ai fait une tentative sur le même projet, sxtool. J’ai un peu galéré pour le lancer et je n’ai pas trouvé la ligne magique où il comprend tous les imports de mon projet. La première exécution m’a retourné 1 500 lignes d’erreurs. La très grande majorité sont des erreurs de style (lignes trop longues, nommage des variables pas en snake_case, docstring manquantes, absence de fin de ligne en fin de fichier…). En désactivant les erreurs de style les plus courantes, je tombe sur quelques erreurs plus intéressantes du type :

  • trop de return dans une fonction ;
  • trop de chemins d’exécution dans une fonction ;
  • variables ou import inutilisés ;
  • redéfinition de nom pré-intégrés, format, file ;
  • clause d’attrapage d’exception trop large ;
  • else inutile après un return.

Les problèmes de code signalés sont légitimes. Ils correspondent à du code peu lisible et des erreurs liées au manque de familiarité avec Python de l’auteur initial. Le code correspondant a été développé par un stagiaire qui débutait en Python.

Mais, une fois ce constat fait, il est totalement irréaliste d’imaginer passer du temps à rectifier le code en question. Ce serait très coûteux en temps, et le bénéfice reste modeste. Exiger du code avec un style parfaitement conforme est l’apanage de quelques rares projets ou entreprises de logiciel très exigeantes. Le reste du monde vit très bien avec du code aux styles complètement hétérogènes (et je suis le premier à le regretter). Essayez de le mettre en place dans une équipe et vous verrez ! C’est ce qui fait que les linters Python, bien qu’existant depuis longtemps, ne sont pas plus populaires que cela. Honnêtement, se faire rappeler à l’ordre par un outil parce qu’il manque une espace après une virgule, c’est très pénible.

L’approche récente du reformatage prise par black et consorts résout ce problème de façon plus pérenne.

Concernant l’analyse de la complexité du code, j’aime beaucoup le concept, mais j’imagine mal le mettre en place. Sur mes projets solo, je suis déjà attentif à la complexité et la lisibilité, donc il ne m’apportera rien. Sur des projets en équipe, les gens qui vont être favorables à la mise en place d’un tel outil sont justement ceux qui sont conscients du problème de la complexité du code, et qui ont déjà tendance à ne pas privilégier ce style. Les non-favorables tombent vite dans des guerres de chapelle (« mais si, sept if imbriqués, c’est très bien ! ») et on ne s’en sort pas. Les cas qui me paraissent réalistes pour la mise en place seraient ceux où des managers sont conscients des bénéfices d’un code peu complexe et imposent l’outil. Ou alors une équipe qui aborde une base de code héritée importante et qui souhaite cibler les modules où le risque de bug est plus élevé qu’ailleurs.

Restent, enfin, les erreurs que peut détecter pylint. On en trouve la liste dans la documentation de référence. Les classes d’erreur ont l’air intéressantes bien que certaines soient un peu louches à mon goût : je vois pas bien comment du code pourrait tourner avec certaines des erreurs qui sont signalées. J’imagine qu’elles sont pourtant toutes basées sur des cas réels.

Voici quelques erreurs prises au hasard :

  • nonlocal-and-global (E0115): Emitted when a name is both nonlocal and global ;
  • not-in-loop (E0103): Used when break or continue keywords are used outside a loop ;
  • return-in-init (E0101): Used when the special class method init has an explicit return value ;
  • inherit-non-class (E0239): Used when a class inherits from something which is not a class ;

pylint peut être lancé avec -E pour ne signaler que les erreurs de ce type. Serait-ce parce que c’est sa plus grande valeur ajoutée ?

Mon bilan

Le concept est intéressant mais le côté pédant de l’outil est désagréable et les autres bénéfices restent trop réduits. Je pense que mettre en place des revues de code sera plus efficace et plus constructif que de passer un projet à pylint.

pyflakes

PyFlakes est avec pylint un des anciens linter/checker de code Python : les premières versions datent de 2009. Le principe est simple : un programme simple qui vérifie les fichiers source Python à la recherche d’erreurs. En complément, le README ajoute il ne va jamais se plaindre à propos du style, il va essayer très très intensément de ne jamais émettre de faux positifs .

Retour de Philippe

Tout ça est très prometteur. Par contre, la documentation ajoute que pyflakes est plus limité dans les types d’erreurs qu’il peut trouver, car il inspecte l’arbre de syntaxe plutôt qu’importer le code.

Voyons voir ce que ça donne, je prends le même cobaye, sxtool. Pas de problème à l’installation, pas de problème à l’exécution. Il me signale une cinquantaine d’erreurs qui ne correspondent en fait qu’à deux cas :

  • un nom est importé mais pas utilisé ;
  • une variable ou un argument est inutilisé.

Intéressant, mais pas fantastique. Mon projet n’a pas d’erreur manifeste, c’est cool.

Je jette un coup d’œil à la documentation pour en savoir plus sur le potentiel de pyflakes pour découvrir qu’il n’y a pas de documentation. Impossible de savoir quelles classes d’erreurs sont détectées. La lecture du ChangeLog laisse entrevoir quelques idées mais sans plus.

Rien non plus sur la configuration, on ne peut pas ignorer certains fichiers ou annoter une ligne pour ignorer une erreur. Il me semble que c’est parce que pyflakes ne s’utilise plus tel quel. Le projet a fait cause commune avec un autre linter, pep8, pour former flake8, un lanceur de linter/checker Python. flake8 est couvert dans la suite de la dépêche et c’est lui qui permet de configurer finement la vérification d’un fichier et la désactivation de certaines erreurs. Par contre, la documentation de flake8 n’en dit pas plus sur les types de vérifications effectuées par pyflakes.

En conclusion, je n’ai pas pu mettre en évidence l’intérêt de pyflakes, mais je sais qu’il a plutôt une bonne réputation dans la communauté Python. Mon projet cobaye est aussi assez simple, il n’utilise que très peu de fonctionnalités de Python. Sur des projets plus élaborés fonctionnellement, j’imagine qu’il peut trouver des erreurs intéressantes.

Si vous utilisez pyflakes et que vous connaissez sa valeur, n’hésitez pas à nous le partager dans les commentaires.

flake8

Flake8 est un lanceur de linter. Il est né du rapprochement des projets pyflakes et pep8 (qui est devenu pycodestyle au passage). La version de base fait du 3-en-1 :

  • PyFlakes, la recherche d’erreurs générales ;
  • pycodestyle, les vérifications de style façon pep8 ;
  • le script McCabe de Ned Batchelder, la vérification de la complexité du code.

Flake8 exécute tous les outils en lançant la commande unique flake8. Il affiche les avertissements par fichiers dans une sortie commune.

Il ajoute également quelques fonctionnalités :

  • les fichiers qui contiennent cette ligne sont ignorés :
# flake8 : noqa
  • les lignes qui contiennent un commentaire # noqa à la fin n’émettront pas d’avertissement ;
  • vous pouvez ignorer des erreurs spécifiques sur une ligne avec # noqa : , par exemple, # noqa : E234. Plusieurs codes peuvent être donnés, séparés par une virgule, le jeton noqa n’est pas sensible à la casse, les deux points avant la liste des codes sont nécessaires, sinon la partie après noqa est ignorée ;
  • des hooks Git et Mercurial ;
  • extensible via les points d’entrée flake8.extension et flake8.formatting.

Configuration

Exemple de fichier de configuration :

[flake8]
max-line-length = 88
select = C,E,F,W,B,B9
ignore = E203, E501, W503
exclude = __init__.py

Ce contenu peut être glissé dans un fichier .flake8, ou dans tox.ini ou encore un setup.cfg, ce qui permet de s’intégrer dans un fichier de config partagé avec d’autres outils de l’écosystème de packaging python.

La force de flake8, c’est qu’on peut facilement rajouter des plugins pour compléter son travail. Il existe des plugins pour tout un tas de vérifications supplémentaires, pour lancer d’autres outils ou pour adapter le format de sortie à des services spécifiques.

Retour de Philippe

flake8 a une bonne réputation dans l’écosystème Python. Je l’ai essayé toujours sur mon projet cobaye sxtool. Je n’ai récupéré que des erreurs de style, et une ou deux variables non utilisées. En forçant le test de complexité à maximum 5, j’ai récupéré une erreur due à la complexité trop élevée d’une fonction.

Je suis plutôt déçu. Les erreurs de style ne m’intéressent pas, je les traiterai avec black. Mais, pas moyen de les ignorer toutes d’un coup. Pas d’erreurs de codage reporté, c’est bien pour mon projet, mais je n’ai toujours aucune idée du type d’erreur qui peut être détecté. Pour la complexité, pylint avait trouvé plus de fonctions nécessitant un retravail, je suis également déçu.

L’écosystème de plugin est réputé riche, mais là encore, la documentation n’en mentionne presque aucun. Vous pouvez piocher dans la longue liste de awesome-flake8-extensions pour trouver votre bonheur. On trouve pas mal de plugins pour lancer d’autres outils dans flake8, genre pylint ou mypy ou encore bandit. On trouve aussi pas mal de plugins pour ajuster le format de sortie à un besoin spécifique, et encore des plugins pour faire quelques vérifications très ciblées.

bandit

Bandit est un outil conçu pour trouver de failles de sécurité connues dans du code Python. Comme Pylint, il analyse les fichiers Python en construisant leur arbre syntaxique (AST) et exécute un ensemble de vérification sur ce dernier. Le projet existe depuis 2015 et a reçu plus de 3000 étoiles GitHub.

Bandit est extensible par plugin, à la fois pour rajouter des vérifications ou pour modifier le format de sortie. On le configure par un fichier en YAML ou par des directives dans les fichiers ou lignes de code concernées.

Retour de Philippe

Avant de lancer le projet, je note déjà que la documentation est bien faite et couvre les aspects qui m’intéressent facilement. Alors, toujours sur mon projet sxtool, que donne bandit ?

 > bandit -r sxtool
[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: None
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    running on Python 3.8.8
Run started:2021-05-15 16:49:52.455989

[...]
--------------------------------------------------
>> Issue: [B318:blacklist] Using xml.dom.minidom.parse to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.dom.minidom.parse with its defusedxml equivalent function or make sure defusedxml.defuse_stdlib() is called
   Severity: Medium   Confidence: High
   Location: .\src\utils.py:20
   More Info: https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b313-b320-xml-bad-minidom
19              try:
20                  self.tree = dom.parse(fileName)
21              except sax.SAXParseException :

--------------------------------------------------

Code scanned:
        Total lines of code: 3073
        Total lines skipped (#nosec): 0

Run metrics:
        Total issues (by severity):
                Undefined: 0.0
                Low: 5.0
                Medium: 1.0
                High: 1.0
        Total issues (by confidence):
                Undefined: 0.0
                Low: 0.0
                Medium: 0.0
                High: 7.0
Files skipped (0):

Donc sept problèmes majeurs! Quand même! Si vous voulez voir les détails, c’est ici.

Les catégories de problème trouvés :

  • utilisation de assert, alors qu’en compilant avec -O, les asserts disparaissent ;
  • lancement d’une commande dans un shell de façon non sécurisée ;
  • utilisation d’une bibliothèque non sécurisées pour parser du XML, notamment vulnérables à de l’injection de code.

Tout ça est présenté dans un magnifique rapport, avec des belles couleurs selon le niveau de vulnérabilité estimé. Et on peut aussi en faire une version html, ou CSV. Pour chaque erreur, un lien vers la documentation indique la raison pour laquelle c’est un risque de sécurité, et pour la plupart la marche à suivre pour le corriger !

Conclusion : j’adore ! Le niveau de finition est très agréable !

Annotation de type

Depuis 2006 et la version 3.0 de Python, il est possible de rajouter des annotations au code Python. Et depuis 2009, Dropbox livre un outil de vérification d’annotations de type pour Python : mypy.

Au fil des versions de Python, les annotations de type se sont généralisées (d’abord des arguments de fonctions à maintenant toutes les variables et attributs de classes) et simplifiées dans leur usage. Mypy a aussi continué à évoluer, permettant des définitions de plus en plus fines des types pour décrire un contenu. En parallèle, le nombre de projet avec du typage disponible n’a cessé d’évoluer, que ce soit directement dans le projet typeshed qui regroupe les informations de types (typing stubs) de la lib standard ou alors livré en même temps avec le paquet concerné (comme Flask mentionné récemment dans une dépêche), ou encore via un paquet séparé qui ne fournit que les stubs (cas de PyQt5-stubs et django-stubs qui fournissent les stubs de respectivement PyQt5 et django).

Pour faire bref, ajouter des annotations de type à votre code va apporter les avantages suivants :

  • les annotations créent une forme de documentation très compact des arguments et résultats des fonctions ;
  • les annotations permettent de garantir que votre code est utilisé de la bonne façon ; corollaire, ça permet de découvrir des bugs difficiles à trouver autrement, quand une fonction/méthode est utilisée de façon incorrecte, ou quand l’ensemble des types possibles d’une variable a mal été pris en compt ;
  • les IDE peuvent utiliser les annotations de types pour proposer une complétion plus intelligente.

Bien sûr, tout cela a un coût :

  • annoter une base de code est très rapide et simple au début, mais peut devenir assez fastidieux et chronophage si on vise le 100 % annoté. Heureusement, les outils fonctionnent très bien avec du code partiellement annoté ;
  • pour les cas complexes, il faut se pencher pas mal sur la documentation. C’est assez chronophage ;
  • certaines constructions dynamiques de Python, ou tout simplement la logique de votre code peut être impossible à capturer avec du typage statique ;
  • certaines annotations sont longues à écrire, et alourdissent la lisibilité du code : des définitions de fonctions vont passer de une ligne à plusieurs à cause de cela ;
  • la vérification de la cohérence globale, c’est un outil de plus à lancer, qui plus est un outil qui n’est pas forcément rapide, donc ça ralentit le processus de développement global.

Depuis quelques années, Dropbox et Facebook/Instagram se sont mis au typage statique de tout leur code, et les retours des développeurs sont très positifs. Il y a eu plusieurs sessions au PyCon US sur ce sujet.

Pour comprendre l’intérêt du typage statique en Python, prenons un exemple simple:

def is_equal(a, b):
    if a == b: 
        return True

Bien qu’imparfaite, cette fonction va plutôt bien marcher sur tout ce qui implémente correctement l’égalité : les booléens, les entiers, les chaînes de caractères, etc. C’est pas mal, et ça veut aussi dire qu’on peut facilement passer à côté d’un bug. Voyons en pratique :

def print_is_equal(a, b):
    if is_equal(a, b):
        print('Egalité pour', a)
    else:
        print('Différence:', a, b)
>>> print_is_equal(1, 1)
Egalité pour 1

>>> print_is_equal(1, 2)
Différence: 1 2

>>> print_is_equal('abc', "abc")    # deux représentations de la même chaîne de caractère sont identiques
Egalité pour abc

>>> print_is_equal(0.3, 0.3)
Egalité pour 0.3

>>> print_is_equal(0.3, 0.2 + 0.1)
Différence: 0.3 0.30000000000000004

Oups ! Et oui, comme 0.1 se représente mal en base 2, il génère des erreurs dans les calculs. Donc, il faut éviter d’utiliser notre belle fonction avec des flottants. C’est ce que peut nous aider à faire les vérificateurs d’annotations de type.

Si on rajoute un brin de documentation et des annotations de type, voilà ce que ça donne :

def is_equal(a: int, b: int) -> bool:
    '''Compare two integers and return True if they are equal'''
    if a == b: 
        return True


def print_is_equal(a: int, b: int) -> None:
    '''Display whether two numeric values are equal'''
    if is_equal(a, b):
        print('Egalité pour', a)
    else:
        print('Différence:', a, b)


print_is_equal(1, 1)
print_is_equal(1, 2)
print_is_equal(0.3, 0.2 + 0.1)

Et lorsqu’on passe ce programme à travers mypy :

>mypy src\is_equal.py
src\is_equal.py:1: error: Missing return statement
src\is_equal.py:16: error: Argument 1 to "print_is_equal" has incompatible type "float"; expected "int"
src\is_equal.py:16: error: Argument 2 to "print_is_equal" has incompatible type "float"; expected "int"
Found 3 errors in 1 file (checked 1 source file)

Il détecte bien, d’une part que la fonction est utilisée de façon incorrecte avec des flottants, d’autre part que nous avons oublié un return : la fonction renvoie None dans le cas d’une inégalité.

À noter que la documentation des deux fonctions est correcte sans être assez précise: two numeric value peut aussi bien faire référence à deux flottants qu’à deux entiers. De même, pour is_equal(), ce que retourne la fonction en cas d’inégalité n’est pas documenté et va fonctionner dans tous les tests qui ne vérifient pas exclusivement l’égalité àFalse. Le développeur avait sûrement en tête de retourner False, mais difficile d’en être sûr. C’est l’intérêt des annotations de type : elles obligent à plus de rigueur et elles capturent l’intention du développeur mieux que de la documentation.

Si vous voulez vous mettre à l’annotation de type dans Python, on trouve pas mal de ressources sur Internet, dont une conférence en français réalisée par un certain Philippe F.

Penchons-nous maintenant sur les outils de l’annotation de type.

mypy

Mypy est la référence en termes de vérification de typage. C’est l’émergence de mypy qui a permis aux annotations de s’imposer dans l’écosystème Python. L’outil est maintenu par l’équipe Python de Dropbox (dans laquelle Guido Van Rossum a fait un séjour assez long). Le projet est très dynamique, avec des nouvelles versions fréquentes, qui permettent de pousser le typage de plus en plus finement. Mypy fournit une documentation de bonne qualité pour aider à se mettre au typage. L’outil dispose d’un large jeu d’option, qui permettent d’ajuster assez finement le niveau de typage qu’on souhaite, de très léger à très exigeant. C’est un peu comme les options de compilation de gcc, il y en a pour tous les goûts. Tout ça peut se régler aussi par un beau fichier de config au format ini.

Mypy gère les annotations Python 3 (directement dans le code) ou Python 2 (sous forme de commentaire). Comme pour les vérificateurs de code, il est possible directement depuis le code d’ignorer une erreur en ajoutant un commentaire # type: ignore. On peut même préciser le type d’erreur à ignorer plus précisément.

Ça se lance en ligne de commande, mais comme l’outil est lent sur des grosses bases de code, on peut lancer un serveur dmypy qui va garder en cache les résultats intermédiaires et vérifier le code beaucoup plus vite.

Ah oui, et c’est écrit en Python, c’est pour ça que c’est lent ! (attention un troll velu s’est caché dans la phrase précédente, à toi de le débusquer sans le nourrir !).

Retour de Philippe

Sans surprise, je suis un grand fan de l’annotation de type et j’utilise mypy intensément. Il s’installe très simplement avec pip. À l’usage, sur mes projets, au fur à mesure que j’y rajoute les annotations de type, j’ai constaté que :

  • c’est chronophage, notamment au début, où on se perd dans la documentation, et à la fin quand on essaye d’être 100 % typés, on croise des cas vraiment complexes à annoter ;
  • c’est bien documenté et on trouve facilement de l’aide, sur le site de mypy ou sur stackoverflow ;
  • il faut parfois modifier le code pour aider mypy avec quelques asserts, c’est sans conséquence et dans un certain nombre de cas, ça oblige à se poser les bonnes questions : est-ce que ma variable trucmuche peut encore être à None ou pas dans cette partie de code ?
  • je n’ai pas l’impression d’avoir trouvé des gros bugs avec ça, par contre, je sais que ma base de code est beaucoup plus fiable. Au boulot, j’ai récemment fait du « refactoring » sur des « callback » un peu poilus et j’étais content que mypy me pointe tous les endroits où je devais intervenir ;
  • l’aspect documentation compacte est incroyablement agréable. Mes collègues qui arrivent sur mes projets avec annotation de type me disent aussi qu’ils ont beaucoup plus de facilité pour comprendre le code. J’ai retravaillé un petit projet à moi de 15 ans d’âge récemment. J’étais à moitié perdu dans mon code de l’époque. J’ai décidé de le typer pour m’y retrouver mieux et ça a fait une vraie différence.

Sur ce petit projet de 15 ans d’âge, je vous montre le code avant. C’est un tout petit bout de code qui doit déplacer une pièce d’un jeu.

    def move_tile(self, pid, d):
         if not self.move_enabled: 
             return

        self.map.move( pid, d )
        self.board.move( pid, d )

        [...]

En revoyant ce code, je ne me rappelais plus ce qu’était pid et d. Avec les annotations, ça donne :

    def move_tile(self, pid: str, d: Tuple[int, int]) -> None:
         if not self.move_enabled: 
             return

        assert self.map
        self.map.move( pid, d )
        self.board.move( pid, d )

        [...]

Avec ces informations, j?

par Philippe F, Oliver, bayo, Ysabeau, Yves Bourguignon, Atem18, Benoît Sibaud, Gil Cot, tisaac, vaxvms, François GUÉRIN, gusterhack, yPhil, patrick_g

DLFP - Dépêches

LinuxFr.org

Présentation de flusio, un média social pour organiser votre veille

 -  10 juin - 

J’ai débuté le développement de flusio il y a un an. Son objectif est d’offrir un espace en ligne permettant à la fois de faire sa veille de manière (...)


Quel lien entre souveraineté numérique et logiciel libre ?

 -  9 juin - 

Le sujet de la souveraineté numérique, vue comme une autonomie stratégique pour l’Union européenne et les États membres dans l’espace du numérique, est (...)


Pétrolette 1.3

 -  7 juin - 

Depuis la dernière version majeure de Pétrolette, nombre de nouveaux sites d’information ont fait leur apparition, particulièrement dans l’alt-tech (...)


Enregistrer les langues du monde village par village avec Lingua Libre

 -  7 juin - 

Lingua Libre est un site soutenu par l’association Wikimédia France. Il vise à faciliter l’enregistrement audio de prononciation de mots. En mars (...)


Sortie de YunoHost 4.2

 -  1er juin - 

La sortie de YunoHost 4.2 est l’occasion de rappeler l’existence de ce projet et de tenir au courant de ses dernières évolutions. On note la (...)