Python est langage fantastique, mais il y a un point qui laisse à désirer par rapport aux environnements plus récents comme node ou rust : le gestionnaire de package. Pip, le gestionnaire de package de python, quoique simple d’utilisation et fiable, pose rapidement des problèmes. On va voir dans cet article les projets qui essayent de proposer une solution plus robuste: Pip-tools, Pipenv et Poetry. Mais d’abord voyons les difficultés que l’on va immanquablement rencontrer avec Pip.

Les problèmes avec Pip

Pour déclarer les dépendances d’une application python, l’usage est de créer un fichier requirements.txt listant tous les packages nécessaires avec leur version

redis==2.10.6
rq==0.13

Pour spécifier la version, Pip propose plusieurs options:

  • on peut mettre une version exacte (par exemple redis==2.10.6)
  • ou des bornes pour définir une plage de versions acceptables, par exemple redis>=2.1.3,<3 accepte toutes les versions supérieures à la version 2.1.3 et inférieures à la version 3.0.0

Il est cependant vivement recommandé d’utiliser des versions exactes pour éviter que les versions sélectionnées par Pip changent sans prévenir au cours du temps.

Pour l’installation, on utilise ensuite la commande :

pip install -r requirements.txt

L’isolation des applications

Ici se pose le premier problème, Pip n’a pas de notion d’application ou de projet: si on utilise naïvement cette commande depuis deux applications, Pip va mixer les dépendances des deux applications et créer un système généralement inutilisable. Pour isoler chaque application, il est donc nécessaire d’utiliser un autre outil: virtualenv ce qui va compliquer tout de suite la création de l’environnement de développement et l’installation de l’application en production.

Les jeux de dépendances multiples

Un autre problème commun est de distinguer differents jeux de dépendances; un cas qui se pose pour tous les projets est de distinguer les dépendances de développement (comme le framework de test, les linters de code …) des dépendances nécessaires à l’execution de l’application en production. La solution la plus simple est d’utiliser plusieurs fichiers de dépendances et d’installer les fichiers un par un selon le besoin; c’est simple mais il faut s’assurer manuellement que tous les fichiers de dépendances sont compatibles.

Les mises à jour

Comme toutes les versions des packages doivent être manuellement spécifiées dans les fichiers requirements, il est très facile de créer des incompatibilités et Pip, s’il détecte le problème, n’aide pas du tout à le résoudre. Nous allons prendre cet exemple:

redis==2.10.6
rq==1.0

Ces deux packages sont incompatibles car rq dépend de redis>=3.0.0. Voilà le comportement de Pip:

$ pip install -r requirements.txt
Collecting redis==2.10.6 (from -r requirements.txt (line 1))
  Using cached https://files.pythonhosted.org/packages/3b/f6/7a76333cf0b9251ecf49efff635015171843d9b977e4ffcf59f9c4428052/redis-2.10.6-py2.py3-none-any.whl
Collecting rq==1.0 (from -r requirements.txt (line 2))
  Using cached https://files.pythonhosted.org/packages/ee/f6/dbcf2a28e5621e1fcf6be6937da9777ad9ab03c7d3cb7d6ee835adc43329/rq-1.0-py2.py3-none-any.whl
Collecting click>=5.0 (from rq==1.0->-r requirements.txt (line 2))
  Using cached https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl
ERROR: rq 1.0 has requirement redis>=3.0.0, but you'll have redis 2.10.6 which is incompatible.
Installing collected packages: redis, click, rq
Successfully installed click-7.0 redis-2.10.6 rq-1.0

Pip affiche bien un message d’erreur, mais le package rq est maintenant inutilisable et il va falloir trouver à la main, par essai-erreur, une combinaison de versions qui fonctionne.

On se retrouve généralement dans cette situation en essayant de faire une mise à jour : on modifie la version d’un package et la nouvelle version introduit une incompatibilité. Ce problème rend les mises à jour de versions dans les fichiers requirements pénibles et dangereuses. Cela n’incite bien sûr pas à les faire régulièrement.

Les packages résiduels

Au fil du temps, les dépendances d’une application vont évoluer : on va rajouter des dépendances mais aussi en supprimer et Pip ne fournit aucun moyen utilisable pour supprimer une dépendance. Pip a bien une commande uninstall mais :

  • la commande ne supprime que le package demandé, pas ses dépendances puisqu’il ne peut pas déterminer si elles sont encore utilisées ou non
  • même si on arrive à établir la liste des packages à supprimer il faut encore arriver à programmer les exécutions des commandes uninstall sur tous les environnements de développement, test et production.

En pratique, le seul moyen de s’assurer que l’environnement ne contient pas de packages résiduels est de supprimer le virtualenv et de relancer l’installation complète. Ces packages résiduels peuvent induire deux types d’erreurs :

  • on utilise sans s’en rendre compte un package non déclaré dans le requirements.txt et tout fonctionne jusqu’à ce que l’on crée un nouvel environnement
  • la simple présence d’un package peut altérer le comportement d’un autre : on peut donc avoir de subtiles différences de comportement entre les environnements amenées par la présence d’un package résiduel

Pip-tools

Ce projet est le plus ancien des trois et propose deux utilitaires: pip-compile et pip-sync.

Le principe de fonctionnement est le suivant :

  • on va créer un fichier de dépendances similaire au requirements.txt, mais au lieu de fixer des versions, on va utiliser des bornes. Par convention, on appelle ce fichier requirements.in :

      redis>=2.10.6,<3
      rq>=0.8
    
  • On lance pip-compile. Il va rechercher un jeu de versions qui satisfait toutes les contraintes spécifiées dans le requirements.in et créer le fichier requirements.txt

      $ pip-compile requirements.in
      #
      # This file is autogenerated by pip-compile
      # To update, run:
      #
      #    pip-compile
      #
      click==7.0                # via rq
      redis==2.10.6
      rq==0.8.2
    
    le fichier requirements.txt est bien créé avec des versions fixes et contient aussi les dépendances de deuxième niveau
    click dans l’exemple est une dépendances de rq.
  • On installe le fichier requirements.txt avec la commande pip-sync. pip-sync est similaire à la commande pip install -r requirements.txt mais va aussi supprimer tous les packages non déclarés dans le requirements.txt.

Dans sa résolution de dépendance, pip-compile prend automatiquement en compte le fichier requirements.txt existant: on peut donc relancer pip-compile à tout moment sans craindre qu’il modifie de manière imprévue les versions enregistrées dans le requirements.txt. Les mises à jour doivent être faites explicitement :

  • soit avec la commande pip-compile --upgrade pour mettre à jour tous les packages d’un coup
  • soit avec pip-compile --upgrade-package <package> pour ne mettre à jour qu’un seul package

Contrairement à Pip qui n’hésite pas à installer des packages incompatibles, pip-compile échoue de manière explicite quand on lui demande une combinaison de packages impossible. Sur l’exemple d’incompatibilité redis/rq qu’on a vu précédemment, voilà ce qu’il se passe:

$ cat requirements.in
redis>=2.0,<3.0
rq>=1.0

$ pip-compile requirements.in
Could not find a version that matches redis<3,>=2.0,>=3.0.0
Tried: 0.6.0, 0.6.1, 1.34, 1.34.1, 2.0.0, 2.2.0, 2.2.2, 2.2.4, 2.4.0, 2.4.1, 2.4.2, 2.4.3, 2.4.4, 2.4.5, 2.4.6, 2.4.7, 2.4.8, 2.4.9, 2.4.10, 2.4.11, 2.4.12, 2.4.13, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.7.1, 2.7.2, 2.7.3, 2.7.4, 2.7.5, 2.7.6, 2.8.0, 2.9.0, 2.9.1, 2.10.0, 2.10.1, 2.10.2, 2.10.3, 2.10.5, 2.10.5, 2.10.6, 2.10.6, 3.0.0, 3.0.0, 3.0.0.post1, 3.0.0.post1, 3.0.1, 3.0.1, 3.1.0, 3.1.0, 3.2.0, 3.2.0, 3.2.1, 3.2.1
There are incompatible versions in the resolved dependencies.

Malheureusement la résolution de dépendances va parfois échouer alors qu’une solution existe et ce, même sur des cas apparemment simples. Par exemple, ce fichier provoque une erreur :

redis>=2.0,<3.0
rq>=0.8

La solution ici est redis==2.10.6 et rq==0.12.0

Comme on peut le voir, les pip-tools adressent uniquement les problèmes de mise à jour de versions et de packages résiduels. Pour isoler son environment et gérer différents jeux de dépendances, il faudra recourir au mêmes techniques qu’avec Pip.

Pipenv

Le second outil que nous allons voir est Pipenv. Pipenv est une création de Kenneth Reitz, l’auteur de la célèbre librairie requests, cela lui a valu une popularité rapide et aussi d’essuyer des critiques sur sa stabilité. Pipenv a maintenant plus de deux ans d’existence et le rythme des changements a aujourd’hui largement diminué.

L’outil s’appuie sur deux fichiers: Pipfile et Pipfile.lock. Le premier sert à déclarer les dépendances du projet et le second à enregistrer les versions concrètes de tous les packages. Voici un exemple de Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
redis = ">=3,<4"
rq = ">=1,<2"

[requires]
python_version = "3.6"

L’installation des dépendances se fait simplement avec la commande:

pipenv install

Pipenv prendra automatiquement en charge la création d’un virtualenv pour isoler les packages du projet et, si pyenv est disponible sur le système, installera aussi la version de python requise dans le Pipfile. Cette version de python dans le Pipfile est importante : c’est un prérequis à l’installation, si Pipenv n’arrive ni à détecter ni à installer la version demandée, la commande échouera.

La création automatique du virtualenv est optionnelle, si la commande est invoquée alors qu’un virtualenv est actif, Pipenv opérera directement sur cet environnement. Cela permet d’associer facilement cet outil avec d’autres gestionnaires de virualenv (comme virtualenvwrapper ou pyenv-virtualenv)

La commande install ne fait qu’installer de nouveaux packages, pour supprimer les packages résiduels, Pipenv a une commande dédiée :

pipenv clean

S’il est possible d’éditer le fichier Pipfile à la main, cela n’est généralement pas nécessaire. On peut passer un nom de package à la commande install:

pipenv install request>=2.20

Dans ce cas Pipenv ajoutera la dépendence au fichier Pipfile, installera le package et mettra à jour automatiquement le fichier Pipfile.lock.

En ajoutant l’option --dev à cette commande, Pipenv enregistrera la dépendance comme une dépendance de développement. Les dépendances de développement sont installées uniquement quand l’option --dev est passée:

pipenv install --dev

Cela permet de gérer deux jeux de dépendances: un pour la production et un pour le développement. Il n’est malheureusement pas possible de créer d’autres jeux de dépendances, Pipenv se limite à ces deux ensembles.

Les mises à jour de packages se font avec la commande update, comme avec pip-compile on peut soit:

  • tout mettre à jour avec: pipenv update
  • ou mettre à jour un seul package: pipenv update rq

La résolution des dépendances souffre des mêmes problèmes que pip-compile. Il échoue lui aussi sur l’exemple redis/rq:

$ pipenv install 'rq>=0.8' 'redis>=2.0,<3.0'
[...]
[pipenv.exceptions.ResolutionFailure]: Warning: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies.
  First try clearing your dependency cache with $ pipenv lock --clear, then try the original command again.
 Alternatively, you can use $ pipenv install --skip-lock to bypass this mechanism, then run $ pipenv graph to inspect the situation.
  Hint: try $ pipenv lock --pre if it is a pre-release dependency.
ERROR: ERROR: Could not find a version that matches redis<3,>=3.0.0
Tried: 0.6.0, 0.6.1, 1.34, 1.34.1, 2.0.0, 2.2.0, 2.2.2, 2.2.4, 2.4.0, 2.4.1, 2.4.2, 2.4.3, 2.4.4, 2.4.5, 2.4.6, 2.4.7, 2.4.8, 2.4.9, 2.4.10, 2.4.11, 2.4.12, 2.4.13, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.7.1, 2.7.2, 2.7.3, 2.7.4, 2.7.5, 2.7.6, 2.8.0, 2.9.0, 2.9.1, 2.10.0, 2.10.1, 2.10.2, 2.10.3, 2.10.5, 2.10.5, 2.10.6, 2.10.6, 3.0.0, 3.0.0, 3.0.0.post1, 3.0.0.post1, 3.0.1, 3.0.1, 3.1.0, 3.1.0, 3.2.0, 3.2.0, 3.2.1, 3.2.1
There are incompatible versions in the resolved dependencies.

La gestion d’erreur dans ce cas n’est d’ailleurs pas satisfaisante:

  • le message d’erreur complet fait plus de 40 lignes et est difficilement lisible
  • Pipenv laisse l’environnement dans un état inutilisable: le Pipfile est modifié et les package redis==2.10.6 et rq==1.0 sont installés. Pour revenir dans un état stable, il faudra annuler les modifications dans le Pipfile et relancer un pipenv install
  • les conseils donnés dans le message d’erreur ne sont pas adaptés

Enfin il faut noter une limitation importante de Pipenv : un projet doit être associé à une seule version de python. Pour les projets qui doivent supporter différentes versions de python (c’est généralement le cas des librairies), son usage n’est pas conseillé (voir par exemple https://github.com/pypa/pipenv/issues/1911).

Voilà pour le tour d’horizon des fonctionnalités de Pipenv. Malgré quelques limitations, il facilite largement la gestion des packages d’un projet python et apporte des solutions simples d’usage aux principaux problèmes de pip qu’on l’on a vu au premier paragraphe.

Poetry

Poetry est le dernier-né de ces outils et est très similaire à Pipenv dans son usage.

On déclare cette fois les dépendances dans le fichier pyproject.yml et les versions concrètes sont enregistrées dans le fichier poetry.lock. Voilà un exemple de fichier pyproject.yml :

[tool.poetry]
name = "test-poetry"
version = "0.1.0"

[tool.poetry.dependencies]
python = "^2.7"
redis = ">=3,<4"
rq = ">=1,<2"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

Comme pour Pipenv, l’installation des dépendences se fait avec la commande :

poetry install

et de la même manière que Pipenv, Poetry va automatiquement créer un virtualenv pour le projet ou réutiliser le virtualenv actif.

On ajoute des dépendances avec la commande add :

poetry add "request >=2.20"

La commande add accepte aussi l’option --dev pour déclarer une dépendance de developpement.

Enfin, les mises à jour s’appliquent avec :

poetry update [package]

Maintenant, voyons les différences notables avec Pipenv.

Tout d’abord, poetry n’a pas à ce jour de commande pour supprimer les packages résiduels, il faudra comme avec Pip, recréer l’environnement.

La résolution des dépendances est par contre plus robuste. Poetry arrive à trouver la solution correcte sur l’exemple redis/rq

$ poetry add 'redis >2,<3' 'rq >=0.8'

Updating dependencies
Resolving dependencies... (0.1s)

Writing lock file

Package operations: 2 installs, 0 updates, 0 removals

 - Installing redis (2.10.6)
 - Installing rq (0.12.0)

Par contre, cela a un prix : la résolution peut parfois prendre un temps considérable en particulier avec des packages ayant de nombreuses versions. Par exemple la commande suivante va mettre près de 5 minutes avant de détecter l’incompatibilité entre les versions demandées d’awscli et pyyaml :

poetry add 'pyyaml >4' 'awscli <=1.16'

Pour cette commande, Poetry va tester toutes les versions de awscli pour en chercher une compatible avec pyyaml>4.

Une autre différence importante est la possibilité d’utiliser l’outil avec différentes versions de python. Il faudra alors déclarer les versions que l’on supporte dans le pyproject.yml :

[tool.poetry.dependencies]
python = "^2.7 || ^3.6"

et s’assurer qu’on lance la commande poetry avec la version de python souhaitée activée (par exemple en utilisant pyenv ou en modifiant son PATH).

Si un package n’est disponible qu’avec python 2 ou python 3, il faudra le déclarer au moment de l’ajout :

poetry add python-saml --python ^2.7
poetry add python3-saml --python ^3.7

Dans sa résolution des dépendances, Poetry ne prend pas en comptes les versions de python que supportent les packages : les deux packages seront résolus, mais uniquement l’un des deux sera installé selon la version de python active.

Enfin, Poetry fournit aussi la possibilité de créer des packages : dans ce cas le pyproject.toml remplace le traditionnel fichier setup.py. Il faudra par contre une version récente de Pip (>=18.0) pour arriver à installer ces packages.

Malgré toutes ses similarités, Poetry fournit donc une alternative intéressante à Pipenv, en particulier si l’on travaille sur une librairie.

Conclusion

On a présenté ces 3 outils de manière chronologique et on peut constater une progression en matière de fonctionnalités, mais cela ne veut pas dire que les plus anciens sont obsolètes :

  • Pip-tools, grace à sa simplicité, permet de conserver une grande flexibilité et sera certainement plus facile à intégrer à des projets existants
  • Pipenv, si vous n’êtes pas affecté par ses limitations, est probablement une solution plus mature et complète que Poetry

Python est enfin doté d’outils satisfaisants pour installer et mettre à jour les packages mais il reste encore la difficulté de choisir une de ces solutions et de la mettre en oeuvre : à l’avenir on aimerait que pip intègre les fonctionnalités de ces outils.