Boostez votre qualité de code Python avec ruff

Au fil des années et de sa professionnalisation, l’écosystème Python s’est progressivement doté d’une multitude d’outils d’analyse statique de code destinés à aider les équipes à maintenir un code de qualité : Flake8, pylint, isort, Mypy, Pyright, Black, Bandit… Ces outils remplissent des rôles variés, allant de la vérification des styles de code à la détection d’erreurs potentielles. Cependant, leur utilisation simultanée peut parfois ralentir les flux de travail et augmenter la complexité de la configuration.

C’est là que ruff intervient, avec l’ambition de devenir un outil unique pour les remplacer tous.

xkcd CC-BY-NC

ruff est un outil apparu en 2022 qui vise à remplacer les linters et les formatters existants, avec l’accent mis sur la rapidité d’exécution, grâce à une écriture en Rust plutôt qu’en Python. Il implémente à ce jour déjà plus de 800 règles de vérification, reprises de Pyflakes, pycodestyle, mccabe, isort, pydocstyle, pyupgrade, Flake8 (et plusieurs dizaines de ses plugins), Bandit, eradicate, Pylint, tryceratops, flynt, refurb, pydoclint… Et comme si ce n’était pas assez, il commence aussi à intégrer ses propres règles supplémentaires, notamment pour cibler des frameworks populaires, comme FastAPI ou NumPy.

Malgré ce grand nombre de règles, il s’exécute extrêmement rapidement, et est capable de traiter la plupart des bases de codes en moins d’une seconde. Ainsi, il analyse toute la base de CPython en à peine 160ms, contre près de 12s pour Pyflakes et plus d’une minute pour Pylint.

Côté formatage, l’écosystème Python était déjà largement unifié derrière Black, qui s’était imposé notamment grâce au choix radical de faire un formatter qui impose ses choix, quasiment sans aucune configuration possible, évitant ainsi aux équipes de toujours longues et souvent stériles discussion sur les règles de formatage à adopter… ruff reprend ce précepte et propose un formatage quasiment identique à celui de Black (les écarts étant volontaires et documentés), mais avec une vitesse d’exécution décuplée (littéralement !). Il est très légèrement plus configurable, en ajoutant principalement un paramètre qui pourrait aider à convaincre certaines équipes de l’adopter : il est possible de lui faire privilégier les ‘simple-quotes’ plutôt que les « double-quotes » pour délimiter les chaînes, mais aussi de lui faire préserver au cas par cas le ‘type’ de « quotes » choisis par le développeur. Le fait d’imposer un style unique était en effet un point souvent reproché à Black.

À noter également qu’une partie des divergences par rapport à Black restent rétrocompatibles avec Black : ruff introduit des modifications dans le code, mais ces modifications sont acceptées par Black, qui n’imposera pas un retour en arrière. Ce qui permet d’envisager dans certains cas de faire cohabiter les deux outils si nécessaire. C’est une situation que j’ai par exemple rencontrée sur un projet où ruff commençait à être adopté du côté des développeurs, tandis que Black restait la référence dans les workflows de CI/CD.

To ruff or not to ruff?

D’un point de vue fonctionnel, la décision d’adopter ou non ruff dès maintenant dépendra beaucoup des outils que vous utilisez actuellement.

Vous n’utilisez pas encore de linter ?

Il serait temps de mettre en place un linter dans votre chaîne de production logicielle ! ruff est alors un choix pertinent, puisque vous n’aurez pas à gérer les écarts par rapport à vos outils existants, et les excellentes performances de ruff vous permettront de l’ajouter à vos workflows sans impact significatif sur leurs vitesse. Attention toutefois à ne pas être trop gourmand dès le début : commencez par n’activer que les règles par défaut de ruff, puis une fois votre code mis en conformité ajoutez progressivement les règles qui vous intéresse. Vous éviterez ainsi d’être découragés par la masse de travail que peut représenter l’adaptation d’une base de code ayant déjà un long historique…

Vous utilisez principalement Flake8 ou des outils proches ?

Là encore, basculer vers ruff est un choix pertinent. Vous gagnerez en vitesse, tout en gardant une excellente compatibilité avec votre existant.

Vous utilisez beaucoup Pylint ?

Face à Pylint, le choix est plus difficile. Même si ruff a commencé à reprendre des règles de Pylint, il n’ambitionne pas, au moins à court terme, de le remplacer totalement. En effet, Pylint n’est pas qu’un simple linter, il effectue aussi un travail de vérification de type (avec la particularité de le faire en grande partie sans tenir compte des annotations de type), ce que ruff a fait le choix de ne pas réaliser pour l’instant.

Si votre code est proprement typé, la solution peut alors être d’utiliser une combinaison de ruff pour le lint et de Mypy pour la vérification du typage.

Dans le cas contraire, restez plutôt sur Pylint. Vous pouvez aussi faire un compromis en utilisant ruff dans les environnements de développement, pour une analyse plus rapide, et Pylint dans la CI/CD, pour une analyse plus complète.

Et pour le formatage ?

Que vous utilisiez déjà ou pas un outil de formatage de code, le remplacer par ruff peut être intéressant pour gagner en confort grâce à ses performances. Je mettrai toutefois un bémol dans le cas où votre code ne bénéficierait pas d’une couverture de test élevé : alors que Black garanti une exécution du code à l’identique après reformatage, grâce à une comparaison rigoureuses des AST produits par le code original et le code formaté, ruff n’offre à priori pas ce type de garantie. Il y a donc un léger risque lors de sa mise en place sur une large base de code, même si l’objectif de production de code quasi identique à celui produit par Black devrait éviter de trop mauvaises surprises.

Pourquoi ne pas adopter ruff ?

L’outil parfait n’existant pas, il y a malheureusement aussi quelques bonnes raisons de ne pas adopter ruff. Ces raisons sont principalement sa gouvernance et le choix du langage Rust.

Pour ce qui est de la gouvernance, contrairement à la plupart des outils que ruff ambitionne de remplacer, ruff n’est pas un projet purement communautaire. Il est développé sous l’égide d’Astral, une entreprise new-yorkaise. Même si Astral semble à priori sincère dans sa mise en avant de son goût pour l’ouverture et sa volonté de continuer à proposer des outils open-source et gratuits, il viendra forcément un moment où la question du modèle économique devra se poser.

Bien sûr, le choix d’une licence très permissive (MIT) garanti qu’en cas de divergence entre les aspirations d’Astral et les volontés de la communauté, un fork sera toujours possible. Et c’est là que le choix du langage Rust pose question. S’il est indéniable que ce choix était nécessaire pour atteindre les performances voulues, il rendra aussi plus difficile le fork, puisque, de fait, bon nombre de pythonistas ne seront pas aptes à reprendre un projet écrit en Rust. Ce défaut est toutefois tempéré par la popularité croissante de Rust dans la communauté Python, où il remplace de plus en plus souvent le C et le C++ pour le développement de bibliothèques hautes performances. On peut notamment citer le cas de Pydantic, en grande partie réécrit en Rust, ou encode de tokenizers, la très populaire bibliothèque de tokenization d’Hugging Face.

Enfin, il se pose la question de l’avenir des outils existants. ruff s’en inspire largement pour se développer rapidement, mais son succès pourrait justement les menacer, en détournant leur base utilisateur. À cet égard, il pourrait donc être intéressant que la gouvernance de ruff soit à l’avenir partagée avec la PSF et PyCQA pour aider à pérenniser l’expérience acquise avec les autres outils.

Pour ma part, j’ai d’ores et déjà adopté ruff sur la plupart de mes projets, et notamment ceux d’OBI Partner, même si je reste vigilant sur les trois points ci-dessus. Alors si vous aussi vous souhaitez sauter le pas et avez besoin d’assistance pour la réalisation, n’hésitez pas à nous contacter pour en discuter.

Vous êtes prêt ? Alors pipx install ruff !

Comment patcher une dépendance Composer sans la forker ?

Lorsque vous travaillez sur un projet PHP utilisant Composer pour la gestion des dépendances, il peut arriver que l’une de vos bibliothèques présente un bug ou nécessite une modification spécifique pour répondre à vos besoins. La solution immédiate qui vient souvent à l’esprit est de forker le dépôt, y apporter les modifications nécessaires, puis de l’utiliser comme source dans votre projet.

Cependant, cette approche n’est pas toujours idéale. Forker une dépendance signifie prendre en charge sa maintenance, suivre ses mises à jour et gérer les éventuelles incompatibilités futures. Cela peut rapidement devenir un fardeau, surtout si vous devez maintenir plusieurs forks.

Heureusement, il existe une alternative élégante : appliquer un patch directement sur la dépendance, sans avoir besoin de forker. Cette méthode vous permet de corriger ou modifier le comportement de la bibliothèque tout en continuant à bénéficier des mises à jour officielles.

Dans cet article, nous explorerons comment utiliser Composer et les outils associés pour appliquer des patchs à vos dépendances de manière efficace, maintenable et sans complexifier votre workflow. Que vous soyez développeur débutant ou expérimenté, cette technique pourra vous faire gagner du temps et simplifier la gestion de vos projets.

4 étapes pour réaliser votre patch

1 – Installation des packages nécessaires au patch

composer require cweagans/composer-patches symplify/vendor-patches --dev

2 – Préparer le fichier de patch

Dans le dossier de vos vendors, une fois le bon fichier identifié, créez une copie de ce dernier en le suffixant par « .old« 

cp vendor/package/toto/src/toto.php vendor/package/toto/src/toto.php.old

Il suffit maintenant de modifier le fichier « .php » et d’appliquer vos modifications.

Seuls les fichiers *.php sont chargés, et non les fichiers *.php.old. De cette manière, vous pouvez vous assurer que le nouveau code fonctionne correctement avant de générer les patchs.

3 – Création du patch

Exécuter la ligne suivante :

vendor/bin/vendor-patches generate

Cette ligne va générer un fichier dans le dossier patches, voici un exemple :

/patches/package-toto-src-toto.php.patch

Le chemin du patch est créé à partir du chemin du fichier original, ce qui garantit que le nom du patch est toujours unique.

De plus, la configuration pour cweagans/composer-patches est ajoutée à votre fichier composer.json :

{
    "extra": {
        "patches": {
            "package/toto": [
                "patches/package-toto-src-toto.php.patch"
            ]
        }
    }
}

4 – Réinstallation des package et vérification du patch

Il suffit de vérifier que tout fonctionne en remettant au propre le code.

Le plus simple est de supprimer le dossier vendor et de relancer l’installation des packages:

rm -Rf vendor
composer install

Et voilà, vous êtes maintenant un maître dans l’art de patcher vos dépendances sans tout casser (ou presque) ! Avec cette méthode, vous pourrez corriger des bugs plus vite que votre collègue ne corrige ses fautes de frappe dans Slack. Alors, à vos claviers, et que vos projets soient aussi stables qu’un serveur un vendredi soir à 18h ! 🚀

Laravel – Routes Signées

key, castle, security

Depuis la version 5.6, Laravel vous permet de mettre en place des URL dites signées, nous allons voir comment programmer tout cela.

Pourquoi utiliser des URL signées ?

Quelques exemples, vous souhaitez, par exemple, réaliser un sondage avec un lien unique, utilisable une seule fois, envoyé à chacun de vos utilisateurs. Ou encore, vous souhaitez confirmer une inscription à une newsletter.

Comment faire ?

Dans cet article, nous allons imaginer qu’un utilisateur a besoin de valider une information, mais que pour ce faire, il n’a pas besoin d’être connecté, mais nous devons être capables de l’identifier de manière unique et que l’information reste fiable.

Étape 1 : Utilisation du middleware "signed"

Pour Laravel < 11.x

Dans le fichier « Http/Kernel.php » ajouter le code suivant (il est possible que vous ayez déjà des middlewares inclus pour les routes).

					protected $routeMiddleware = [
 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
];
				

Pour Laravel >= 11.x

Ce middleware n’est plus à ajouté et il est directement intégré dans les routes (voir chapitre ci-dessous)

Étape 2 : Créer une route

Dans un fichier de gestion des routes, par exemple « route/web.php », on ajoute une route qui va prendre en compte le middleware « signed ».

					use Illuminate\Http\Request;

Route::get('newsletter/confirm/{email}/{user}', function ($email, $user) {
    if (! $request->hasValidSignature()) {
        abort(401);
    }
   // Action à ajouter
})->name('confirmation.email')->middleware(['signed']);
				

Générons aussi un moyen de créer un lien signé pour pouvoir faire des tests, le code ci-dessous est un exemple.

					use \Illuminate\Support\Facades\URL;

$url = URL::temporarySignedRoute('confirmation.email', now()->addHour(), [
    'email' => 'test@test.com',
    'user' => 1
]);
				

Étape 3 : Vérification de la route

La route est vérifiée automatiquement via la gestion des routes, si la signature n’est pas bonne, Laravel lèvera une erreur de type : Illuminate\Routing\Exceptions\InvalidSignatureException.

Comment est définie la signature ?

La signature est un hachage HMAC (sha256) de l’URL, elle utilise comme clé de chiffrement la clé secrète de l’application enregistrée dans le fichier .env de l’application.