Poetry comme gestionnaire de projet

Publié le 29/05/2021

Cela fait quelques temps que j'entends et vois parler de Poetry comme remplaçant de pip et de venv. Après avoir un peu analysé le bidule, il y a plusieurs points très, très intéressants. En vrac:

  • La publication de paquets sur pypi https://aricodes.net/posts/python-package-from-scratch/
  • L'intégration des environnements virtuels
  • Du pattern matching sur les dépendances, comme ce que fait npm

Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes. Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante Je pense notamment aux environnements virtuels: ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), venv (la version intégrée depuis la version 3.3 de l'interpréteur, et la manière recommandée de créer un environnement depuis la 3.5).

Boilerplate

Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml. TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.

Démarrage d'un nouveau projet

La commande poetry new <project> créera une structure par défaut relativement compréhensible:

$ poetry new django-gecko
$ tree django-gecko/
django-gecko/
├── django_gecko
│   └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
    ├── __init__.py
    └── test_django_gecko.py

2 directories, 5 files

Ceci signifie que nous avons directement (et de manière standard):

  • Un répertoire django-gecko, qui porte le nom de l'application que vous venez de créer
  • Un répertoires tests, libellé selon les standards de pytest
  • Un fichier README.rst (qui ne contient encore rien)
  • Un fichier pyproject.toml, qui contient ceci:
[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... <...@grimbox.be>"]

[tool.poetry.dependencies]
python = "^3.9"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

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

Initialisation pour un projet existant

La commande poetry init permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.

Dependency management made easy

La manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet. Ce fichier reprend, ligne par ligne, chaque dépendance et la version nécessaire. Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==, <= et >=.

Poetry propose un épinglage basé sur SemVer. Les contraintes qui peuvent être appliquées aux dépendances sont plus touffues que ce que proposent pip -r, avec la présence du curseur ^, qui ne modifiera pas le nombre différent de zéro le plus à gauche:

  • ^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
  • ^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
  • ...

L'avantage est donc que l'on spécifie une version majeure - mineure - patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à la prochaine version majeure - mineure patchée (non incluse 😉).

Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources: de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.

Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte) et de qualité (tous les environnements utilisent la même version d'une dépendance).

Ajout d'une dépendance

L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la commande poetry add <dep>:

$ poetry add django
Using version ^3.2.3 for Django

Updating dependencies
Resolving dependencies... (5.1s)

Writing lock file

Package operations: 8 installs, 1 update, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing attrs (21.2.0)
  • Installing more-itertools (8.8.0)
  • Installing packaging (20.9)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Updating django (3.2 -> 3.2.3)
  • Installing pytest (5.4.3)

Elle est ensuite ajoutée à notre fichier pyproject.toml:

[...]

[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"

[...]

Et contrairement à pip, pas besoin de savoir s'il faut pointer vers un fichier (-r) ou un dépôt VCS (-e), puisque Poetry va tout essayer, dans un certain ordre. L'avantage également (et cela m'arrive encore souvent, ce qui fait hurler le runner de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la dépendance que l'on vient d'installer parmi les fichiers de requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande add.

Python packaging made easy

Cette partie dépasse mes compétences et connaissances, dans la mesure où je n'ai jamais rien packagé ni publié sur pypi.org. Ce n'est pas l'envie qui manque, mais les idées et la nécessité 😉. Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.

Les chapitres 7 et 8 de Expert Python Programming - Third Edtion, écrit par Michal Jaworski et Tarek Ziadé en parlent très bien:

Python packaging can be a bit overwhelming at first. The main reason for that is the confusion about proper tools for creating Python packages. Anyway, once you create your first package, you will se that this is as hard as it looks. Also, knowing proper, state-of-the-art packaging helps a lot.

En gros, c'est ardu-au-début-mais-plus-trop-après. Et c'est heureusement suivi et documenté par la PyPA (Python Packaging Authority).

Les étapes sont les suivantes:

  1. Utiliser setuptools pour définir les projets et créer les distributions sources,
  2. Utiliser wheels pour créer les paquets,
  3. Passer par twine pour envoyer ces paquets vers PyPI
  4. Définir un ensemble d'actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier setup.py, et définir les propriétés du projet ou de la librairie dans le fichier setup.cfg.

Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé 🤪): poetry build et poetry publish:

$ poetry build
Building geco (0.1.0)
  - Building sdist
  - Built geco-0.1.0.tar.gz
  - Building wheel
  - Built geco-0.1.0-py3-none-any.whl

$ tree dist/
dist/
├── geco-0.1.0-py3-none-any.whl
└── geco-0.1.0.tar.gz

0 directories, 2 files

Ce qui est quand même 'achement plus simple que d'appréhender tout un écosystème.

En conclusion

Il y a plein de bonnes idées, qui demandent en même temps quelques changements d'habitudes (comme tout nouveau produit, vous me direz). En parallèle, la lisibilité globale de la sortie console est franchement améliorée et beaucoup plus claire/agréable à lire et parcourir. Le fait aussi que la gestion des dépendances soit simplifiée est agréable aussi. Ce qui est peut-être un chouia dommage, c'est que cela cela redéfinisse un nouveau standard et ne se base pas ou n'améliore pas les fichiers de dépendances actuels. Mais le plus important concerne l'orientation que prend le langage et l'écosystème vers l'utilisation de standards, et peut-être qu'abandonner des pratiques non-suivies par l'industrie constituera un gain pour la suite.

Ressources