#!⌨

La gestion de ses dépendances en Python

Je viens de donner une présentation sur la gestion des dépendances en Python au cours de la conférence Pytong 2015 à Lyon. Mon support de présentation est disponible en ligne ici. Et cet article permet de développer le contenu de cette présentation.

Nos projets Python se basent en général sur d'autres librairies ou frameworks. Ces autres briques ont leurs propres cycles de release et leurs propres systèmes de versions. Le problème intervient quand elles évoluent dans le temps : une librairie peut évoluer alors que vous référencez une version plus ancienne. De la même manière, dans une équipe, il y a un risque que vos collègues travaillent avec d'autres versions que celle avec laquelle vous travaillez, ce qui peut induire des problèmes chez certains et pas chez d'autres.

Il y a quelques années, lors de l'ajout d'une nouvelle dépendance dans un environnement virtuel, on fesait un pip freeze > requirements.txt, puis un git commit pour se souvenir de l'ajout de la dépendance. Le problème avec cette approche, c'est que quelques années plus tard, on se retrouve avec une dépendance qui est resté bloquée dans le passé (et travailler avec Django 1.3.2 en 2015, c'est pas marrant !).

Les versions

Pour gérer correctement ses dépendances, il faut avoir les idées claires sur la gestion des versions. Dans l'écosystème python, la PEP 440 définit la manière de définir des versions. Grosso modo, elle définit des numéros de versions à plusieurs composants (entiers positifs ou nuls), séparés par des "." et pouvant avoir des suffixes (pour les versions alphas, betas et release candidates). Cette spécification nous permet d'avoir des numéros de versions cohérents dans l'écosystème Python.

Plus globalement, la gestion sémantique de version a émergé ces derniers temps dans le monde du développement logiciel. Cette spécification propose de gérer les versions avec 3 composants X.Y.Z, avec une signification particulière pour chacun de ses composants :

Ces notions de changements d'API sont très important dans la maintenance d'une application. La gestion sémantique de version nous aide alors énormèment à réaliser nos mises à jour : tant que X ne change pas, on peut faire une mise à jour sans craintes.

Pour vos développements, je vous conseille vivement d'utiliser la gestion sémantique de version pour les briques publiques. Vous pouvez toutefois vous permettre plus de simplicité pour les brisques privées (avec seulement deux composantes pour un numéro de version). Quoi qu'il en soit, restez contant dans le nombre de composants (en tout cas, si vous voulez changer de schéma de versions, faites le lors de la release d'une version majeure).

Certains logiciels ont des schéma de versions un peu fantaisistes. Par exemple, les versions de TeX convergent vers π à chaque nouvelle release. C'est à dire qu'on passe de la version 3.1 à la 3.14 ainsi de suite... TeX en est actuellement à la version 3.14159265.

Pour illustrer le changement de schéma de versions à la légère, Shinken est récemment passé de la version 2.4 à la version 2.4.1. Il se trouve que je package Shinken pour ArchLinux. Pour le gestionnaire de package de cette distribution, la versions 2.4.1 est antérieur à la version 2.4, et empêche donc une mise à jour standard (les plus trolleurs diront que c'est un bug d'ArchLinux).

Dépendances avec Python

La gestion simple

setuptools nous propose un outil simple et suffisant pour définir quelles sont les dépendances d'une brique logicielle. Il suffit de renseigner les champs install_requires, extras_requires, tests_require, et setup_requires dans notre fichier setup.py (attention à l'incohérence des pluriels entre les deux mots de chaque option !).

Renseigner ces valeurs nous permet alors d'utiliser pip install mon_projet et pip install --upgrade mon_projet pour installer et mettre à jour une installation.

Bien que setuptools l'autorise, évitez de fixer les versions des dépendances dans le fichier setup.py. Ce listing doit indiquer quelle est la dépendance et dans quelle famille de version cela devrait fonctionner, le choix de la version finale étant laissé à l'utilisateur. Nous pouvons par contre borner une version. Par exemple : un projet peut être connu pour fonctionner très bien entre Django 1.7 et 2.0 (potentiel changement d'API), on rentrera alors django>=1.7,<2.0.

Il faut également noter qu'on ne doit renseigner que les dépendances directes de notre projet, et pas les dépendances des dépendances.

Cette gestion simple peut se montrer suffisante dans des cas où l'équipe de développeurs est restreinte à une ou deux personnes et où la personne qui met en production fait également des tests avec les dernières versions des logiciels.

Cependant, cela peut paraitre simpliste lorsqu'on se trouve dans une équipe plus large. En effet, quand un plus grand nombre de personnes commence à travailler ensemble sur un projet, il faut s'assurer que les dépendances soient uniformes dans l'ensemble de l'équipe pour assurer le reproduction du comportement d'un poste à un autre.

Cette gestion peut également entrainer des problèmes de différences de versions entre le poste de développeur et les environnements de préproduction ou de production, et donc une différence de comportement.

pip-tools

pip-tools est un outil qui propose d'améliorer la gestion des dépendances en proposant à la fois une gestion des dépendances directes sans préciser de versions exactes et une gestion où on épingle chaque version de manière précise.

Les dépendances directes sont placées dans un fichier requirements.in, éventuellement avec des conditionnant bornant leurs versions. On lance alors pip-compile qui va créer un fichier requirements.txt en épinglant chacune des versions. On place ces deux fichiers dans notre gestionnaire de sources et on peut alors suivre l'évolution des versions des dépendances.

pip-tools propose également un utilitaire pip-sync qui synchronise les paquets installés dans un environnement virtuel avec ceux spécifiés dans le requirements.txt (en supprimant ceux qui n'apparaissent plus, réalisant les mises à jours, etc...).

Enfin, le contenu du fichier requirements.in devrait être le même que dans la clause install_requires de notre fichier setup.py. Vous pouvez alors ne pas vous répéter en lisant le fichier requirements.in lorsque vous préparez les valeurs du fichier setup.py.

requires.io

requires.io est un service web qui vous permet d'être notifié lorsque votre projet n'est plus à jour par rapport à une dépendance. Il existe également des notifications suite à des alertes de sécurité.

L'avantage de ce service est qu'il s'intègre très facilement avec Bitbucket et Github. Il n'est pas cher (tarif max de $20/month pour une organisation Github ou bitbucket, un accès API, et la vérification d'environnement de production).

Enfin, il permet également de définir des règles de gestion personnalisées (par exemple ne pas alerter lors de la sortie de Django 2.0, ...).

À noter tout de même l'existence de la commande pip list --outdated qui affiche les paquets installés qui peuvent bénéficier d'une mise à jour.

Pour aller plus loin

Vous pouvez consulter trois articles de Vincent Driessen, l'auteur principale de pip-tools (également l'auteur de l'article qui fait référence autour de la gestion des branches avec Git) :

Le Python Packaging Guide contient une page sur la différence d'intention entre setup.py/install_requires et un fichier requirements.txt.

Certains auditeurs de la présentation m'ont donné encore plus de liens :