CLI Python avec Click : Guide Complet pour Applications Pro
CLI Python avec Click : Le Guide Ultime
Section titled “CLI Python avec Click : Le Guide Ultime”🎯 Introduction
Section titled “🎯 Introduction”Click révolutionne la création d’applications en ligne de commande (CLI) en Python. Développé par l’équipe derrière Flask, ce framework transforme des scripts Python simples en interfaces CLI robustes et intuitives en quelques lignes seulement.
Pourquoi choisir Click ?
- ✅ Syntaxe basée sur les décorateurs (propre et lisible)
- ✅ Gestion automatique des messages d’aide
- ✅ Validation intégrée des arguments
- ✅ Support des couleurs et styles
- ✅ Testing simplifié avec CliRunner
Dans ce guide, vous apprendrez à :
- Créer vos premières commandes CLI
- Gérer arguments et options efficacement
- Structurer des CLI complexes avec des groupes
- Valider et sécuriser les entrées utilisateur
- Tester professionnellement vos applications
📦 1. Installation et Premiers Pas
Section titled “📦 1. Installation et Premiers Pas”Installation de Click
Section titled “Installation de Click”pip install clickVérification de l’installation :
python -c "import click; print(f'Click version: {click.__version__}')"Votre Première Commande CLI
Section titled “Votre Première Commande CLI”import click
@click.command()@click.option('--name', default='Monde', help='Nom à saluer')def hello(name): """Commande simple qui salue une personne.""" click.echo(f"Bonjour, {name}!")Exécution :
python hello.py --name "Riyad"# Sortie : Bonjour, Riyad!
python hello.py# Sortie : Bonjour, Monde!📝 Note importante :
@click.command()transforme une fonction en commande CLI@click.option()définit une option facultativeclick.echo()est préférable àprint()pour la compatibilité
🛠️ 2. Arguments vs Options : La Différence Essentielle
Section titled “🛠️ 2. Arguments vs Options : La Différence Essentielle”Les Arguments (obligatoires)
Section titled “Les Arguments (obligatoires)”@click.command()@click.argument('prenom')@click.argument('nom')def saluer(prenom, nom): """Salue une personne avec son prénom et nom.""" click.echo(f"Bienvenue {prenom} {nom}!")Utilisation :
python greet.py Riyad Odjouade# Sortie : Bienvenue Riyad Odjouade!❌ Sans arguments :
python greet.py# Click affiche automatiquement : Error: Missing argument 'PRENOM'Les Options (facultatives)
Section titled “Les Options (facultatives)”@click.command()@click.option('--verbose', '-v', is_flag=True, help='Mode verbeux')@click.option('--count', '-c', default=1, type=int, help='Nombre de répétitions')def repeat(verbose, count): """Répète un message.""" if verbose: click.echo("Mode verbeux activé") for i in range(count): click.echo(f"Message {i+1}")Exemples d’utilisation :
python script.py --verbose --count 3python script.py -v -c 3python script.py # Utilise les valeurs par défaut📊 Tableau comparatif :
| Caractéristique | Argument | Option |
|---|---|---|
| Obligatoire | ✅ Oui | ❌ Non |
| Position | Fixe | N’importe où |
| Syntaxe | valeur | --nom valeur |
| Valeur par défaut | Non | Oui |
| Abréviation | Non | -n pour --nom |
🏗️ 3. Groupes de Commandes : Architecture Pro
Section titled “🏗️ 3. Groupes de Commandes : Architecture Pro”Structure de Base
Section titled “Structure de Base”import click
@click.group()def monapp(): """Application de gestion de projet.""" pass
@monapp.command()def init(): """Initialise un nouveau projet.""" click.echo("Projet initialisé")
@monapp.command()@click.option('--env', default='dev', help='Environnement')def deploy(env): """Déploie le projet.""" click.echo(f"Déploiement sur {env}")Arborescence créée :
monapp├── init└── deploy --env [prod|dev|test]Groupes Imbriqués
Section titled “Groupes Imbriqués”@click.group()def db(): """Commandes de base de données.""" pass
@db.command()def migrate(): """Applique les migrations.""" click.echo("Migrations appliquées")
@db.command()def seed(): """Remplit la base avec des données.""" click.echo("Base peuplée")
# Ajout au groupe principalmonapp.add_command(db)Utilisation :
python cli.py db migratepython cli.py db seed🛡️ 4. Validation Avancée des Entrées
Section titled “🛡️ 4. Validation Avancée des Entrées”Types de Validation Intégrés
Section titled “Types de Validation Intégrés”@click.command()@click.option('--age', type=click.IntRange(0, 120))@click.option('--score', type=click.FloatRange(0.0, 100.0))@click.option('--file', type=click.Path(exists=True))def validate(age, score, file): """Exemple de validation multiple.""" click.echo(f"Âge: {age}, Score: {score}, Fichier: {file}")Choix Limités avec click.Choice
Section titled “Choix Limités avec click.Choice”@click.command()@click.option('--role', type=click.Choice(['admin', 'user', 'guest']), prompt='Sélectionnez un rôle')def set_role(role): """Définit le rôle utilisateur.""" click.echo(f"Rôle défini: {role}")Fichiers et Répertoires
Section titled “Fichiers et Répertoires”@click.command()@click.argument('input', type=click.File('r'))@click.argument('output', type=click.File('w'))def process(input, output): """Traite un fichier.""" content = input.read() output.write(content.upper()) click.echo("Fichier traité avec succès")Validation Personnalisée
Section titled “Validation Personnalisée”def validate_email(ctx, param, value): """Valide le format d'email.""" if '@' not in value: raise click.BadParameter("Email invalide") return value
@click.command()@click.option('--email', callback=validate_email)def register(email): """Enregistre un utilisateur.""" click.echo(f"Utilisateur {email} enregistré")🎨 5. Interface Utilisateur Avancée
Section titled “🎨 5. Interface Utilisateur Avancée”Messages Stylisés
Section titled “Messages Stylisés”@click.command()def status(): """Affiche le statut avec style.""" click.secho("✓ Succès", fg="green", bold=True) click.secho("⚠ Avertissement", fg="yellow") click.secho("✗ Erreur", fg="red") click.secho("ℹ Information", fg="blue")Couleurs disponibles :
black,red,green,yellowblue,magenta,cyan,whitebright_*variants
Barres de Progression
Section titled “Barres de Progression”import time
@click.command()def process_files(): """Traite des fichiers avec barre de progression.""" files = list(range(100))
with click.progressbar(files, label='Traitement') as bar: for file in bar: time.sleep(0.01) # Simulation traitementInteractions Utilisateur
Section titled “Interactions Utilisateur”@click.command()def configure(): """Configuration interactive.""" # Demande simple nom = click.prompt('Entrez votre nom', type=str)
# Mot de passe masqué mdp = click.prompt('Mot de passe', hide_input=True)
# Confirmation if click.confirm('Confirmer la configuration?'): click.echo("Configuration sauvegardée") else: click.echo("Annulé")
# Choix multiple lang = click.prompt( 'Langage préféré', type=click.Choice(['Python', 'JavaScript', 'Rust']) )🧪 6. Testing Professionnel avec CliRunner
Section titled “🧪 6. Testing Professionnel avec CliRunner”Setup de Test
Section titled “Setup de Test”import clickfrom click.testing import CliRunner
@click.command()@click.option('--name', default='World')def hello(name): click.echo(f"Hello {name}")
# Testsdef test_hello_default(): runner = CliRunner() result = runner.invoke(hello) assert result.exit_code == 0 assert "Hello World" in result.output
def test_hello_name(): runner = CliRunner() result = runner.invoke(hello, ['--name', 'Riyad']) assert result.exit_code == 0 assert "Hello Riyad" in result.outputTester les Erreurs
Section titled “Tester les Erreurs”def test_validation_error(): @click.command() @click.option('--age', type=int) def check_age(age): if age < 18: raise click.UsageError("Âge minimum: 18")
runner = CliRunner() result = runner.invoke(check_age, ['--age', '16']) assert result.exit_code != 0 assert "Âge minimum" in result.outputTester les Entrées Utilisateur
Section titled “Tester les Entrées Utilisateur”def test_prompt(): @click.command() def ask_name(): name = click.prompt('Nom') click.echo(f"Bonjour {name}")
runner = CliRunner() result = runner.invoke(ask_name, input='Riyad\n') assert "Bonjour Riyad" in result.outputCoverage Complet
Section titled “Coverage Complet”class TestCLI: def setup_method(self): self.runner = CliRunner()
def test_success_flow(self): result = self.runner.invoke(main_command, ['arg1', '--opt', 'val']) assert result.exit_code == 0
def test_error_flow(self): result = self.runner.invoke(main_command, ['invalid']) assert result.exit_code == 2 # Code d'erreur Click
def test_help(self): result = self.runner.invoke(main_command, ['--help']) assert "Usage:" in result.output📁 7. Structure de Projet Professionnelle
Section titled “📁 7. Structure de Projet Professionnelle”mon-cli-app/├── cli/│ ├── __init__.py│ ├── main.py # Point d'entrée│ ├── commands/│ │ ├── __init__.py│ │ ├── db.py # Commandes base de données│ │ └── deploy.py # Commandes déploiement│ └── utils.py # Utilitaires partagés├── tests/│ ├── __init__.py│ ├── test_main.py│ └── test_commands/├── pyproject.toml # Dépendances├── README.md└── setup.pymain.py - Point d’Entrée
Section titled “main.py - Point d’Entrée”import clickfrom .commands import db, deploy
@click.group()def cli(): """Mon application CLI professionnelle.""" pass
# Ajout des groupes de commandescli.add_command(db.group)cli.add_command(deploy.group)
if __name__ == '__main__': cli()Commande Modulaire
Section titled “Commande Modulaire”import click
@click.group()def group(): """Commandes de gestion de base de données.""" pass
@group.command()@click.option('--backup/--no-backup', default=True)def migrate(backup): """Applique les migrations.""" if backup: click.echo("Backup créé") click.echo("Migrations appliquées")Installation en Package
Section titled “Installation en Package”from setuptools import setup, find_packages
setup( name="mon-cli-app", version="1.0.0", packages=find_packages(), install_requires=["click>=8.0.0"], entry_points={ 'console_scripts': [ 'monapp=cli.main:cli', ], },)Installation :
pip install -e .monapp --help # Disponible globalement🚀 8. Bonnes Pratiques et Astuces Pro
Section titled “🚀 8. Bonnes Pratiques et Astuces Pro”1. Documentation Auto-générée
Section titled “1. Documentation Auto-générée”@click.command()@click.option('--host', default='localhost', show_default=True)@click.option('--port', default=8080, show_default=True)def server(host, port): """Démarre le serveur.
Exemples: server --host 0.0.0.0 --port 9000 """ click.echo(f"Serveur démarré sur {host}:{port}")2. Variables d’Environnement
Section titled “2. Variables d’Environnement”@click.command()@click.option('--api-key', envvar='API_KEY', help='Clé API (variable: API_KEY)')def call_api(api_key): """Appelle l'API externe.""" click.echo(f"Clé API: {api_key}")3. Callbacks Contextuels
Section titled “3. Callbacks Contextuels”def validate_db(ctx, param, value): """Valide la connexion DB avant exécution.""" if not check_database_connection(value): raise click.BadParameter("DB inaccessible") return value
@click.command()@click.option('--db-url', callback=validate_db)def backup(db_url): """Crée un backup de la base.""" click.echo("Backup réussi")4. Logging Intégré
Section titled “4. Logging Intégré”import logging
@click.command()@click.option('--verbose', '-v', count=True)def log_demo(verbose): """Démonstration de logging.""" level = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}[verbose] logging.basicConfig(level=level)
logging.debug("Message debug") logging.info("Message info") logging.warning("Message warning")5. Tableau Comparatif des Approches
Section titled “5. Tableau Comparatif des Approches”| Critère | argparse | click | Recommandation |
|---|---|---|---|
| Courbe d’apprentissage | Raide | Douce | ✅ Click |
| Code boilerplate | Beaucoup | Minimal | ✅ Click |
| Validation | Manuel | Intégrée | ✅ Click |
| Testing | Complexe | Simple (CliRunner) | ✅ Click |
| Documentation | Manuel | Auto-générée | ✅ Click |
| Communauté | Standard | Large (Pallets) | ✅ Click |
📋 Évaluation des Connaissances
Section titled “📋 Évaluation des Connaissances”À quoi sert cette évaluation ?
Section titled “À quoi sert cette évaluation ?”Testez votre maîtrise de Click en 3 minutes. Seuil de réussite : 80%.
À vous de jouer ! 🚀
📝 Exercices Pratiques
Section titled “📝 Exercices Pratiques”Exercice 1 : Créer un Gestionnaire de Tâches CLI
Énoncé :
Créez une CLI task avec les commandes :
add: ajoute une tâche (option--priority)list: liste toutes les tâchesdone: marque une tâche comme terminée
Template :
import click
@click.group()def task(): """Gestionnaire de tâches.""" pass
# Votre code ici...Solution
tasks = []
@click.group()def task(): """Gestionnaire de tâches.""" pass
@task.command()@click.argument('description')@click.option('--priority', default='medium', type=click.Choice(['low', 'medium', 'high']))def add(description, priority): """Ajoute une nouvelle tâche.""" task = { 'id': len(tasks) + 1, 'description': description, 'priority': priority, 'done': False } tasks.append(task) click.secho(f"✓ Tâche ajoutée (ID: {task['id']})", fg='green')
@task.command()def list(): """Liste toutes les tâches.""" if not tasks: click.echo("Aucune tâche.") return
for t in tasks: status = "✓" if t['done'] else "○" color = 'green' if t['done'] else 'yellow' click.secho(f"{t['id']}. [{status}] {t['description']} ({t['priority']})", fg=color)
@task.command()@click.argument('task_id', type=int)def done(task_id): """Marque une tâche comme terminée.""" for task in tasks: if task['id'] == task_id: task['done'] = True click.secho(f"Tâche {task_id} terminée !", fg='green') return click.secho(f"Tâche {task_id} introuvable", fg='red')
if __name__ == '__main__': task()Tests :
python tasks.py add "Apprendre Click" --priority highpython tasks.py listpython tasks.py done 1Exercice 2 : Convertisseur Fichier JSON → YAML
Énoncé : Créez un convertisseur CLI qui :
- Prend un fichier JSON en entrée
- Convertit en YAML
- Sauvegarde ou affiche le résultat
Exigences :
- Validation du fichier d’entrée
- Option
--outputpour sauvegarder - Option
--indentpour l’indentation
Solution
import jsonimport clickimport yaml
@click.command()@click.argument('input_file', type=click.Path(exists=True, dir_okay=False))@click.option('--output', '-o', type=click.Path(writable=True))@click.option('--indent', default=2, help='Indentation YAML')def json2yaml(input_file, output, indent): """Convertit JSON vers YAML."""
# Lecture JSON try: with open(input_file, 'r') as f: data = json.load(f) except json.JSONDecodeError: raise click.BadParameter("Fichier JSON invalide")
# Conversion YAML yaml_data = yaml.dump(data, indent=indent, default_flow_style=False)
# Sortie if output: with open(output, 'w') as f: f.write(yaml_data) click.secho(f"✓ Converti vers {output}", fg='green') else: click.echo(yaml_data)
if __name__ == '__main__': json2yaml()Utilisation :
python converter.py data.json --output data.yaml --indent 4python converter.py data.json # Affiche à l'écranExercice 3 : API Client avec Authentification
Énoncé : Créez un client CLI pour une API REST avec :
- Authentification via token
- Commandes
get,post,delete - Gestion des erreurs HTTP
Solution
import clickimport requests
@click.group()@click.option('--token', envvar='API_TOKEN', required=True)@click.pass_contextdef api(ctx, token): """Client API REST.""" ctx.ensure_object(dict) ctx.obj['TOKEN'] = token ctx.obj['BASE_URL'] = 'https://api.example.com'
def make_request(ctx, method, endpoint, **kwargs): """Fait une requête authentifiée.""" headers = {'Authorization': f'Bearer {ctx.obj["TOKEN"]}'} url = f"{ctx.obj['BASE_URL']}/{endpoint}"
try: response = requests.request(method, url, headers=headers, **kwargs) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: click.secho(f"Erreur: {e}", fg='red') ctx.exit(1)
@api.command()@click.argument('resource')@click.pass_contextdef get(ctx, resource): """Récupère une ressource.""" data = make_request(ctx, 'GET', resource) click.echo(json.dumps(data, indent=2))
@api.command()@click.argument('resource')@click.option('--data', required=True)@click.pass_contextdef post(ctx, resource, data): """Crée une ressource.""" try: json_data = json.loads(data) except json.JSONDecodeError: raise click.BadParameter("JSON invalide")
result = make_request(ctx, 'POST', resource, json=json_data) click.secho("✓ Créé avec succès", fg='green') click.echo(json.dumps(result, indent=2))
if __name__ == '__main__': api()Exercice 4 : Monitoring System avec Barre de Progression
Énoncé : Créez un outil de monitoring qui :
- Scanne des serveurs en parallèle
- Affiche une barre de progression
- Sort les résultats dans un tableau
Solution
import clickimport timeimport concurrent.futures
servers = [ 'server1.example.com', 'server2.example.com', 'server3.example.com']
def check_server(server): """Simule une vérification de serveur.""" time.sleep(0.5) # Simulation return { 'server': server, 'status': 'up' if hash(server) % 3 != 0 else 'down', 'response_time': hash(server) % 100 }
@click.command()@click.option('--timeout', default=10, help='Timeout par serveur')def monitor(timeout): """Monitore plusieurs serveurs."""
click.secho("🔍 Scan des serveurs...", bold=True)
with concurrent.futures.ThreadPoolExecutor() as executor: # Barre de progression with click.progressbar( executor.map(check_server, servers), length=len(servers), label='Vérification' ) as results: data = list(results)
# Affichage tableau click.echo("\n" + "="*50) click.secho(f"{'Serveur':<25} {'Statut':<10} {'Temps (ms)':>10}", bold=True) click.echo("-"*50)
for server_info in data: color = 'green' if server_info['status'] == 'up' else 'red' status_icon = '✓' if server_info['status'] == 'up' else '✗'
click.secho( f"{server_info['server']:<25} " f"{status_icon} {server_info['status']:<7} " f"{server_info['response_time']:>10}", fg=color )📢 Partagez cet article
Section titled “📢 Partagez cet article”Dernière mise à jour : 07 Janvier 2026