Python est historiquement un langage très utilisé pour concevoir des applications en ligne de commande (ou CLI pour l'acronyme anglais). Pendant longtemps, le module optparse
de la bibliothèque standard a permis aux développeur·euses de faciliter la conception de telles applications en offrant des outils pour gérer proprement les paramètres des programmes.
Ce module été déprécié avec les versions 2.7 et 3.2 de Python en 2011, au profit de argparse
, projet externe réintégré à ce moment au cœur du langage.
On notera également l'existence du module getopt
, moins utilisé et surtout beaucoup moins complet.
Mais le sujet d'aujourd'hui n'est pas dans bibliothèque standard, puisqu'il s'agit de Python Fire
, un projet de Google dont la première version date de 2017. Contrairement aux exemples précédemment cités, qui se contentent de gérer les paramètres des programmes en CLI, Fire
permet la construction de CLI complètes à partir de n'importe quel code de base.
L'installation se fait facilement via pip
, comme c'est l'usage :
pip install fire
Un exemple simple pour commencer
Considérons tout d'abord un exemple très simple, avec une fonction qui se contente d'incrémenter un entier :
def increment(i: int):
"""
Add 1 to the given integer.
:param i: an integer
:return: i + 1
"""
return i + 1
La façon la plus basique de faire une CLI de cette fonction serait un code de ce style :
import sys
def usage(name):
print(f"Usage: {name} <I>")
if __name__ == "__main__":
if len(sys.argv) != 2:
usage(sys.argv[0])
sys.exit(1)
print(increment(int(sys.argv[1])))
Il faut donc vérifier les paramètres d'entrée, gérer les cas d'erreurs, prendre en charge la conversion de type, et afficher la sortie. Pas vraiment optimal... Utiliser argparse
ici permettrait de simplifier les trois premiers points, mais nous verrons plus tard que cet outil serait tout aussi pénible sur des cas plus complexes.
À présent, voici comment faire avec Fire
:
import fire
if __name__ == "__main__":
fire.Fire(increment)
Et tout est là ! Fire
va se charger de tout, que ce soit de la vérification des paramètres, des erreurs, de l'aide à l'utilisateur, et de l'affichage de la sortie. Les paramètres optionnels (si par exemple le paramètre de la fonction increment
avait eu une valeur par défaut) sont également pris en compte.
$ python test.py
ERROR: The function received no value for the required argument: i
Usage: test.py I
For detailed information on this command, run:
test.py --help
$ python test.py --help
NAME
test.py - Add 1 to the given integer.
SYNOPSIS
test.py I
DESCRIPTION
Add 1 to the given integer.
POSITIONAL ARGUMENTS
I
an integer
NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS
$ python test.py 16
17
Démultiplions les fonctions
Là où toute la puissance de Fire
s'exprime, c'est certainement losqu'il s'agit de gérer plusieurs fonctions en même temps. Ajoutons par exemple une seconde fonction à notre script :
import fire
def increment(i: int):
"""
Add 1 to the given integer.
:param i: an integer
:return: i + 1
"""
return i + 1
def decrement(i: int):
"""
Remove 1 to the given integer.
:param i: an integer
:return: i - 1
"""
return i - 1
if __name__ == "__main__":
fire.Fire()
La seule différence pour Fire
ici est l'absence de paramètre que nous lui donnons. De cette façon, Fire
va simplement prendre en compte toutes les fonctions à sa portée. Lors de l'appel en ligne de commande, il suffira de préciser quelle sous-fonction on souhaite appeler.
$ python test.py increment 16
17
$ python test.py decrement 18
17
Bien sûr, le --help
s'adapte en conséquence.
En comparaison, réaliser la même chose avec argparse
aurait nécessité un bloc if
/else
pour rediriger à la main vers la bonne fonction. Et dans le cas où nos fonctions auraient des signatures différentes, il aurait en plus fallu définir les différents paramètres dans des groupes exclusifs distincts.
La même, avec des classes
Il est possible d'obtenir le même résultat en utilisant des méthodes au lieu de fonctions simples.
class AddOrRemove:
def __init__(self, n):
self.n = n
def add(self, i):
return i + self.n
def remove(self, i):
return i - self.n
if __name__ == "__main__":
instance = AddOrRemove(1)
fire.Fire({
"increment": instance.add,
"decrement": instance.remove
})
Il aurait été possible d'appeler Fire
avec fire.Fire(instance)
, mais il aurait alors fallu utiliser les noms add
et remove
en ligne de commande. L'instanciation de Fire
telle qu'elle est faite dans l'exemple précédent permet de lister les méthodes que l'on souhaite rendre disponibles, ainsi que leur nom d'usage.
Et bien plus encore
Ce n'est qu'un aperçu très bref des fonctionnalités de Fire
, qui bénéficie en plus d'un développement très actif. Il est par exemple possible d'activer l'auto-complétion, par la touche Tab, sur les CLI créés.
Pour aller plus loin, leur documentation compte nombre d'exemples variés.