Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 3 sur 5

Écrire un .pre-commit-config.yaml multi-hooks

Un fil indépendant, pas une suite

Important avant de commencer : ce module ne dépend pas du module 2. C'est un second fil de compétence qui progresse en parallèle du premier — il est simplement placé après dans l'ordre de lecture du parcours. Il suppose uniquement que vous avez terminé les modules 5 (« Installer pre-commit ») et 6 (« Hooks natifs vs pre-commit ») du parcours Débutant. Vous travaillez ici sur un projet entièrement différent de mon-api-node.

Le projet

Un tout petit script Python, volontairement mal formaté pour donner du travail aux hooks :

mkdir mon-script-python
cd mon-script-python
git init

Créez app.py avec exactement ce contenu (les espacements bizarres sont volontaires) :

import os
def   greet(name):
    x = 1
    print( "hello, "+name )
greet('world')

Le schéma de .pre-commit-config.yaml

Un fichier .pre-commit-config.yaml a la structure suivante :

  • repos : une liste de dépôts de hooks
  • pour chaque repo :
    • repo : l'URL du dépôt de hooks (ou une valeur sentinelle comme local)
    • rev : une référence immuable — un tag (v5.0.0) ou un SHA de commit complet. Jamais un nom de branche.
    • hooks : une liste de hooks à activer parmi ceux que ce repo propose, chacun identifié par son id

Pourquoi rev doit être immuable

pre-commit met en cache l'environnement d'installation d'un hook (dépendances, environnement virtuel) en fonction de la valeur de rev, en partant du principe que cette référence ne change jamais. Un tag ou un SHA respecte cette hypothèse. Une branche comme main, non : son contenu bouge, mais pre-commit continuerait à utiliser l'environnement mis en cache au moment de la première installation, sans jamais le rafraîchir — ce qui rend le comportement imprévisible et non reproductible d'une machine à l'autre.

Si vous essayez quand même rev: main, pre-commit ne plante pas immédiatement mais vous avertit très explicitement :

[WARNING] The 'rev' field of repo 'https://github.com/pre-commit/pre-commit-hooks' appears to be a mutable reference (moving tag / branch).  Mutable references are never updated after first install and are not supported.  See https://pre-commit.com/#using-the-latest-version-for-a-repository for more details.  Hint: `pre-commit autoupdate` often fixes this.

C'est un avertissement, pas une erreur bloquante — les hooks s'exécutent quand même sur cette exécution-là. Mais c'est un piège : n'importe qui d'autre exécutant le même .pre-commit-config.yaml plus tard, avec un cache différent, peut obtenir un comportement différent du vôtre. Utilisez toujours un tag ou un SHA.

Étape 1 — les vérifications génériques

Commencez par les hooks les plus simples, indépendants de tout langage, fournis par pre-commit/pre-commit-hooks :

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: check-yaml
      - id: detect-private-key

check-yaml valide que tous les fichiers YAML du dépôt sont syntaxiquement corrects. detect-private-key refuse le commit si un fichier ressemble à une clé privée (RSA, SSH...) — une vérification générique typique, indépendante du langage du projet.

Étape 2 — le formateur

Ajoutez black, le formateur Python standard, qui corrige automatiquement le style plutôt que de simplement le signaler :

  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks:
      - id: black

Étape 3 — le linter

Ajoutez flake8, qui détecte des problèmes que black ne corrige pas — imports inutilisés, variables jamais utilisées :

  - repo: https://github.com/pycqa/flake8
    rev: 7.1.1
    hooks:
      - id: flake8

Le fichier complet, .pre-commit-config.yaml :

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: check-yaml
      - id: detect-private-key
  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks:
      - id: black
  - repo: https://github.com/pycqa/flake8
    rev: 7.1.1
    hooks:
      - id: flake8

Étape 4 — premier run, et la preuve du formateur

Ajoutez les fichiers à l'index et lancez pre-commit sur tout le dépôt :

git add .
pre-commit run --all-files

Sortie réelle obtenue sur ce fichier :

check yaml...............................................................Passed
detect private key.......................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted app.py

All done! ✨ 🍰 ✨
1 file reformatted.

flake8...................................................................Failed
- hook id: flake8
- exit code: 1

app.py:1:1: F401 'os' imported but unused
app.py:5:5: F841 local variable 'x' is assigned to but never used

Remarquez que flake8 s'est quand même exécuté alors que black a échoué juste avant : par défaut, pre-commit exécute tous les hooks configurés dans une même passe, même si l'un d'eux échoue, pour vous donner la liste complète des problèmes d'un coup.

app.py a été réécrit automatiquement par black. Avant/après :

Avant (votre fichier original) :

import os
def   greet(name):
    x = 1
    print( "hello, "+name )
greet('world')

Après (réécrit automatiquement par black) :

import os


def greet(name):
    x = 1
    print("hello, " + name)


greet("world")

black a corrigé l'espacement, les guillemets et les lignes vides — du pur style. Il n'a en revanche touché ni à l'import os inutilisé, ni à la variable x jamais utilisée : ce n'est pas son rôle, c'est celui du linter.

Étape 5 — corriger ce que flake8 signale

black ne peut pas décider à votre place si un import ou une variable est réellement inutile — il vous laisse le corriger. Éditez app.py :

def greet(name):
    print("hello, " + name)


greet("world")

Étape 6 — la preuve finale

git add .
pre-commit run --all-files

Sortie attendue, tout vert :

check yaml...............................................................Passed
detect private key.......................................................Passed
black....................................................................Passed
flake8...................................................................Passed

Ce que vous venez de faire

Vous avez écrit une configuration pre-commit multi-hooks à partir de zéro : un schéma repos/rev/hooks respecté à la lettre, un rev immuable pour chaque repo, et trois rôles distincts (vérification générique, formateur, linter) qui coopèrent dans une même passe. Au module suivant, vous cassez volontairement cette configuration pour apprendre à diagnostiquer une vraie erreur pre-commit — pas une erreur inventée, celle que l'outil produit réellement.

Vérifiez votre compréhension

Ce module dépend-il du module 2 (« Ajouter une Feature ») ?

Dans .pre-commit-config.yaml, à quoi correspond exactement le champ `rev` d'un repo ?

Dans l'exemple de ce module, que se passe-t-il quand black échoue mais que flake8 est aussi configuré ?

Quels sont les trois rôles obligatoires dans la configuration multi-hooks de ce module ?

Envie d'être prévenu des prochains modules ?

L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.