Formation Python | Créer une CLI avec Typer

Typer est une librairie qui permet de créer des interfaces en ligne de commande (CLI) en Python. Découvrez comment créer une CLI avec Typer sans plus attendre !

Icône de calendrier
Intermédiaire
8 chapitres

Typer : un outil pour créer une CLI en Python

Typer est un outil qui permet de créer des interfaces en ligne de commande (CLI) en Python.

Basée sur le typage en Python, Typer permet de créer des CLIs en quelques lignes de code.

Mais avant cela, il est nécessaire d’installer Typer ! 👇

Installation de Typer

Pour installer Typer, il suffit d’utiliser pip au sein d’un environnement virtuel :

copié !
pip install typer

Il est désormais possible de créer notre CLI avec Typer !

Créer une commande de base avec Typer

Une CLI est composée de commandes, définies au sein de fonctions Python qui seront exécutées lors de l’appel de la commande.

Pour créer une commande avec Typer, il faut commencer par importer… 🥁🥁🥁 Typer !

main.py
copié !
import typer

Créons désormais notre application Typer dans une variable app :

main.py
copié !
import typer
app = typer.Typer()

Ensuite, il suffit de définir une fonction Python et de la décorer avec @app.command().

main.py
copié !
import typer
app = typer.Typer()

@app.command()
def hello():
	print("Hello !")

@app.command()
def goodbye():
	print("Goodbye !")

Le nom des fonctions hello et goodbye correspondent aux noms des commandes à exécuter.

Il est également possible d’ajouter une description à la commande en utilisant le paramètre help de la fonction @app.command().

main.py
copié !
@app.command(help="Affiche Hello")
def hello():
	print("Hello !")

Mais alors, comment exécuter une commande avec Typer ?

Il existe deux manières d’exécuter une CLI Typer : en exécutant ce script en ligne de commande ou via le code Python.

Exécuter une commande

En ligne de commande

Pour exécuter une CLI Typer en ligne de commande, il suffit d’exécuter le fichier avec la commande suivante :

copié !
typer <fichier.py> run <nom-commande>

Exécuter une CLI, via une CLI… C’est un peu Inception cette histoire ! 🤯

Un point sur la casse !

Une petite précision sur l’utilisation des casses dans les commandes Typer s’impose.

Lorsque vous utilisez la bibliothèque Typer pour créer des interfaces de ligne de commande (CLI) en Python, les noms de commande sont automatiquement convertis en minuscules pour respecter les conventions de nommage des CLI.

Cela signifie que, quelle que soit la casse utilisée dans le nom de la fonction, le nom de la commande sera toujours en lowercase ou en kebab-case lors de l’appel.

  • camelCase => lowercase
  • snake_case => kebab-case
main.py
copié !
@app.command()
def helloWorld():  # camelCase
	typer.echo("Hello, World!")

@app.command()
def good_bye():  # snake_case
	typer.echo("Goodbye!")

Dans cet exemple, les commandes seront appelées ainsi :

python script.py helloworld
python script.py good-bye

Typer convertit les noms de commande de la sorte afin d’assurer la cohérence et la convivialité des CLI.

Via du code Python

Il est également possible de spécifier au sein du fichier Python que la commande doit être exécutée en appelant la méthode app().

main.py
copié !
import typer
app = typer.Typer()

@app.command()
def hello():
	print("Hello !")

@app.command()
def goodbye():
	print("Goodbye !")

if __name__ == "__main__":
	app()

En Python, __name__ est une variable spéciale dont la valeur dépend de la manière dont le script est exécuté.

  • Si le script est exécuté directement, __name__ est défini sur "__main__".
  • Si le script est importé comme module dans un autre script, __name__ est défini sur le nom du script/module.

La ligne if __name__ == "__main__" permet ainsi de s’assurer que le code qui suit ne sera exécuté que si le script est exécuté directement.

On peut désormais exécuter notre script via la commande classique :

copié !
python <fichier.py> <nom-commande>

Cette méthode présente l’avantage d’être moins verbeuse et de permettre (à long terme comme nous le verrons par la suite) une meilleure organisation du code.

A ce stade, nos commandes sont relativement basiques. Il est temps de les enrichir avec des arguments et des options !

Aide et documentation

L’option --help permet d’afficher des informations d’usage de la CLI.

Pour afficher les commandes disponibles dans la CLI, il suffit d’ajouter l’option --help au fichier de la CLI :

copié !
python <fichier.py> --help

Pour afficher l’aide d’une commande, il suffit d’ajouter l’option --help à la commande :

copié !
python <fichier.py> <nom-commande> --help

Apparaîtront alors les informations relatives à la commande :

  • Nom
  • Description
  • Arguments
  • Options
  • Etc.

Arguments

À la manière des commandes Shell, les arguments sont des valeurs passées à une commande lors de son exécution.

Dans la commande shell cd /home/user, /home/user est un argument de la commande cd.

copié !
python <fichier.py> <nom-commande> arg1 arg2 ...

Déclarer un argument

Pour déclarer un argument, il suffit de déclarer un paramètre dans la fonction correspondant à la commande.

main.py
copié !
def hello(name: str):
	print(f"Hello {name}!")

Ces arguments sont par défaut obligatoires.

Appeler un argument

On peut désormais appeler notre commande de la manière suivante :

copié !
python main.py hello Toto

Si la fonction est appelée sans argument, une erreur sera levée :

Missing argument 'NAME'.

Annoter les arguments

Pour ajouter des annotations à nos paramètres, nous allons utiliser Annotated qui permet de définir des métadonnées pour nos arguments.

Commençons par importer Annotated :

main.py
copié !
import typer
from typing_extensions import Annotated

Annotated peut ensuite être utilisé pour ajouter des annotations à nos arguments.

main.py
copié !
import typer
from typing_extensions import Annotated

def hello(name: Annotated[str, typer.Argument()]):
	# ...

Ces annotations permettent de définir des métadonnées pour nos arguments, comme le fait qu’un argument est optionnel ou encore le fait d’ajouter un message d’aide pour un argument.

Arguments optionnels

Pour définir un argument comme optionnel nous aurons besoin d’importer la librairie typing :

main.py
copié !
import typer
from typing_extensions import Annotated
from typing import Optional

Ensuite, il suffira de :

  1. Déclarer l’argument comme optionnel avec Optional
  2. Lui affecter une valeur par défaut (par exemple None)
main.py
copié !
import typer
from typing_extensions import Annotated
from typing import Optional

def hello(name: Annotated[Optional[str], typer.Argument()] = None):
	if name is None:
		print("Hello mysterious user !")
	else:
		print(f"Hello {name} !")

Si aucune valeur n’est passée à l’argument name, sa valeur sera None et le message par défaut sera affiché.

On pourrait bien entendu définir une valeur par défaut autre que None pour l’argument optionnel. 👇

main.py
copié !
def hello(name: Annotated[str, typer.Argument()] = "World"):
	print(f"Hello {name} !")

Message d’aide

Il est également possible de définir un message d’aide pour un argument en utilisant le paramètre help au sein de typer.Argument() :

main.py
copié !
def hello(name: Annotated[Optional[str], typer.Argument(help="Nom de la personne à saluer")] = None):
	if name is None:
		print("Hello mysterious user !")
	else:
		print(f"Hello {name} !")

Options

Les options sont souvent utilisées pour ajouter des fonctionnalités à une commande et sont la plupart du temps optionnelles.

Dans la commande shell ls -a, -a est une option de la commande ls.

Déclarer une option

Définir des options avec Typer se fera de la même manière que pour les arguments, mais en utilisant typer.Option() à la place de typer.Argument().

main.py
copié !
def hello(
	name: Annotated[str, typer.Argument(help="Nom de la personne à saluer")],
	enthousiast: Annotated[bool, typer.Option(help="Intonation de la phrase")] = False,
	lang: Annotated[str, typer.Option(help="Langue du message")] = "en",
):
	words = {
		"en": "Hello",
		"fr": "Bonjour",
	}
	output = f"{words[lang]} {name}"
	print(enthousiast and f"{output} !" or output)

Appeler une option

Les options agissent de la même manière que les arguments, mais sont précédées de tirets (- ou --) lors de l’appel de la commande.

copié !
python main.py hello Toto --enthousiast

La présence de l’option --enthousiast entraînera l’affichage de la phrase avec un point d’exclamation.

Si l’option était d’un autre type que bool, il serait nécessaire de préciser une valeur à l’option :

copié !
python main.py hello Toto --lang=fr

Il est également possible de définir des alias pour les options au sein de typer.Option() :

main.py
copié !
def hello(
	name: Annotated[str, typer.Argument(help="Nom de la personne à saluer")],
	enthousiast: Annotated[bool, typer.Option(help="Intonation de la phrase")] = False,
	lang: Annotated[str, typer.Option("-l", "--l", help="Langue du message")] = "en",
):
	# ...

Interagir avec la CLI (prompt)

Typer offre également la possibilité d’interagir avec la CLI à travers des options de prompt.

Pour affecter une valeur à une option de manière interactive, il suffit d’ajouter prompt=True au sein de typer.Option().

main.py
copié !
def hello(lang: Annotated[str, typer.Option(prompt=True)] = "en"):
	words = {
		"en": "Hello",
		"fr": "Bonjour",
	}
	print(words[lang])

La CLI vous demandera alors de saisir une valeur pour l’option lang :

> python main.py hello
Lang: 

Si vous souhaitez afficher une phrase sur mesure dans la demande de saisie, au lieu du nom de l’option, il est possible de remplacer True par le texte en question :

main.py
copié !
def hello(lang: Annotated[str, typer.Option(prompt="En quelle langue souhaitez-vous que je vous salue ? (en, fr)")]):
	#...

Aller plus loin

Si vous souhaitez aller plus loin avec Typer, je vous invite à vous rendre sur la documentation officielle de Typer et explorer d’autres fonctionnalités et notions telles que :

  • La customisation des commandes (formats d’affichage et couleurs)
  • La création de barres de progression
  • L’ajout de sous-commandes
  • La prise en compte de valeurs multiples pour les options et arguments
  • L’organisation de ses fichiers
  • La mise en place de tests
  • La création d’un package Python
  • Etc.