L’enfer des paquets Python : des fichiers partout (4 / 7)
La gestion des paquets Python est parfois un enfer. Pour s’en convaincre, il suffit de se noyer quelques minutes dans la myriade de fichiers utilisables (et utilisés !) pour construire ou installer un paquet.
💕💕💕
Cet article fait partie d’une série de 7 articles larmoyants sur la création de paquets Python :
- Le sac de nœuds
- Les racines du mal
- La folie des formats
- Des fichiers partout
- La boîte à outils
- L’expression des besoins
- La solution minimale
Avant toute autre chose, j’aimerais faire un gros bisou à l’ensemble des membres de l’équipe PyPA. Je me plains beaucoup dans cette suite d’articles, cela ne m’empêche pas d’avoir une immense et sincère dose de respect pour le travail sisyphéen déjà accompli.
Ceci étant dit : nous voilà (re)partis pour le chouinage 😭.
💕💕💕
Mais pourquoi ?
On ne peut pas dire qu’il n’existe pas de guide pour créer des paquets en Python. Le problème, ce n’est pas le manque, c’est la profusion. Des guides, vous en trouverez partout, plus ou moins vieux, plus ou moins pratiques, plus ou moins utiles… Le plus dur n’est pas d’en trouver, c’est de tout lire et de piocher différentes informations dans chacun, jusqu’à vous faire votre propre conviction.
Vous croyiez peut-être trouver dans ces lignes un bon récapitulatif de ce qui existe, mais voici la triste nouvelle : vous n’avez ici qu’une source supplémentaire à laquelle vous référer si deux ou trois choses vous plaisent.
Ceci étant, ce n’est déjà pas si mal…
Quel est le rapport de cette introduction avec les fichiers ? Eh bien, c’est assez simple. On ne peut pas dire qu’il n’existe pas de fichier de configuration pour créer des paquets en Python. Le problème, ce n’est pas le manque, c’est la profusion. Des fichiers, vous en trouverez partout, plus ou moins vieux, plus ou moins pratiques, plus ou moins utiles… Le plus dur n’est pas d’en trouver, c’est de tout lire et de piocher différentes informations dans chacun, jusqu’à vous faire votre propre conviction.
(C’est bon, vous l’avez, le rapport ?)
Je ne vais pas vous refaire le coup de « faut les comprendre, les gens
qui font Python, parce que Python c’est vieux, on ne peut pas tout
changer d’un coup… ». C’est un peu vrai pour cet enfer de fichiers,
mais c’est aussi un peu faux.
L’exemple officiel
proposé aujourd’hui par PyPA contient 4 fichiers qui servent pour la
création de paquets (setup.py
, setup.cfg
,
MANIFEST.in
et pyproject.toml
). Si l’on peut
comprendre l’envie de couvrir un maximum de solutions possibles, on
peut tout autant condamner l’impression de chaos intersidéral donnée à
quelqu’un qui voudrait apprendre.
(Rappel : un projet minimal Rust contient un
fichier Cargo.toml
pour les métadonnées et un
fichier src/main.rs
pour le code du projet. De plus, ces
deux petits fichiers sont créés automatiquement pour vous par la
commande cargo new
.)
S’il est vrai qu’il aurait été difficile de penser dès le début à tous les besoins d’un fichier de configuration, il est en revanche beaucoup plus discutable de dire que l’on doit vivre avec ce triste historique pour l’éternité. Contrairement à d’autres sujets, rien ne nous empêcherait de définir un nouveau standard de fichier de configuration. Et rien ne nous empêcherait de faire en sorte que ce nouveau standard permette de générer des paquets identiques à ceux existant. Nous ferions table rase du passé, de ses vieux fichiers et de ses vieux outils, pour n’utiliser qu’un fichier dans tous les cas. Le créateur du paquet s’adapterait à ces nouvelles règles, certes, mais rien ne changerait pour l’utilisateur final, ni pour les outils qu’il utiliserait.
Ce serait beau, n’est-ce pas ? C’est l’heure de la bonne nouvelle : figurez-vous que c’est déjà ce qui s’est passé. Sans blague.
Maintenant que vous avez très envie de connaître la suite (oui, c’est totalement sournois et totalement assumé), nous allons pouvoir nous infliger tout le cheminement de pensée pour arriver à la situation actuelle. Ce qui compte, c’est le chemin, pas la destination, non ?
Une liste pas si longue
Ce n’est pas la peine de râler : comme à chaque fois, nous n’allons pas voir l’intégralité de ce qui a existé pour créer ou installer des paquets. Ne vous attendez pas à une liste exhaustive, juste à quelques fichiers emblématiques qui permettront de comprendre d’où l’on vient.
setup.py
Ce fichier est le premier fichier introduit pour gérer la création de paquets, c’est aussi le plus connu et le plus utilisé aujourd’hui, malgré son grand âge (au moins 20 ans, ça ne nous rajeunit pas).
L’idée derrière setup.py
est relativement simple : pour
mettre en place toute la configuration nécessaire à la création et à
l’installation de paquets, on utilise un script Python qui définit
un ensemble de métadonnées (le nom du paquet, la liste des fichiers à
inclure, etc.) et différentes commandes (créer un paquet source, un
paquet binaire, installer, etc). Pour faire cela, Python propose un
module appelé
distutils
,
qui contient tout ce qu’il faut pour décrire ces métadonnées et ces
commandes. Il suffit de l’importer dans setup.py
,
d’appeler les bonnes fonctions, et le tour est joué.
Seulement, comme nous l’avons déjà vu plusieurs fois avec les outils
gérant les paquets Python, distutils
est assez limité et
ses fonctionnalités ne sont pas strictement définies. Le code devient
pernicieusement la référence de ce que l’on peut faire, et la peur
(légitime) de tout casser empêche rapidement d’ajouter des
fonctionnalités ou de corriger certains dysfonctionnements que d’aucuns
auraient confondus avec des fonctionnalités.
Limité par distutils
, setup.py
aurait pu être
remplacé par une autre solution. Mais on a trouvé
mieux : setuptools
.
setuptools
est un module qui
utilise distutils
en interne, mais qui fournit des
fonctionnalités supplémentaires, telles qu’une gestion plus poussée des
fichiers à inclure, la possibilité de créer des exécutables Windows, et
surtout… la possibilité de définir des dépendances.
Nous verrons les bibliothèques et les outils plus en détail dans le
prochain article, mais il est important de comprendre
que setuptools
va ouvrir sans s’en rendre compte une boîte
de Pandore. Puisque le module est externe à Python, il s’encombre
beaucoup moins des pincettes de son prédécesseur. Les nouvelles
fonctionnalités sont ajoutées au gré des besoins des utilisateurs, dans
une joyeuse désorganisation qui a au moins eu le mérite de permettre
une chaotique mais large diffusion des paquets Python. La bibliothèque
vient avec un exécutable, easy_install
, qui permet
d’installer un paquet et ses dépendances. Elle vient également avec le
format de paquets « egg » que nous avons abordé la dernière fois.
À partir du développement parfaitement anarchique
de setuptools
, il a été impossible de spécifier
correctement les options et les bonnes pratiques de la création de
paquets. setup.py
a les inconvénients de ses avantages :
étant écrit en Python, il permet d’utiliser toute la puissance du
langage, pour ce qui devait à la base être quelques lignes de
métadonnées et de scripts d’installation. Tout ce qui pourrait être
simplement descriptif devient potentiellement dynamique à
l’exécution. Des extensions sont proposées, dépendantes ou pas
de setuptools
, offrant une galaxie de possibilités. Les
scripts grossissent, sont copiés de projet en projet sans être
compris. Des bouts de code corrigeant des dysfonctionnements pour
différentes versions de Python, distutils
ou setuptools
sont inclus dans tous
les setup.py
de la Terre.
Et à la fin, on arrive à ça. Bien sûr, ce projet nécessite beaucoup de configuration et il serait difficile de faire tout ce que fait ce script en moins de code. Bien sûr, il est assez aisé de comprendre l’intégralité de ce fichier, par ailleurs assez joliment écrit, si l’on s’en donne la peine.
Non, le problème de setup.py
n’est même pas sa complexité
potentielle, qui peut dans de rares cas être tout de même utile. Le
véritable problème, c’est qu’il n’y a eu pendant longtemps aucune
alternative simple pour créer des paquets simples en pur
Python. L’unique solution a été d’écrire du code, pour ce qui aurait
souvent pu être totalement déclaratif. Et qui n’a pas été tenté
d’écrire du code, plein de code horrible, même pour faire des choses
simples ? Avec cette montagne de code horrible dans d’innombrables
projets, setuptools
a dû inclure des solutions de
contournement permettant de contourner les solutions de contournement
mises en place dans les scripts pour contourner des problèmes corrigés
depuis. setuptools
a dû copier et inclure différentes
fonctions de différentes versions de Python (y compris leurs bugs, bien
évidemment) pour être parfaitement rétrocompatible. Pour faire
court : setuptools
est devenu un monstre purulent qui a
contaminé les setup.py
d’une bonne majorité de projets.
setup.cfg
Évidemment, l’idée de mettre en place un format déclaratif pour la
création de paquets est arrivée assez vite, et une solution a été
intégrée à setuptools: setup.cfg
.
Ce fichier INI n’est rien d’autre qu’une présentation différente de
la plupart des options proposées en Python par
setuptools
. On va donc y retrouver les mêmes
inconvénients : les mêmes bugs, les mêmes options peu ou pas
documentées, les mêmes incohérences.
Surtout, ce fichier ne vient pas remplacer setup.py
, mais
il vient le compléter. On a besoin de garder le script, même presque
vide ! Si des données sont en double, celles de setup.cfg
sont conservées.
Pourquoi a-t-on besoin de garder le fichier setup.py
?
Tout simplement parce que setuptools
ne fournit pas de
commande externe pour exécuter les commandes intégrées dans le
script. Pour générer un paquet source, on utilise python setup.py
sdist
qui exécute directement le script.
Ce qui pourrait n’être qu’un détail se transforme en problème majeur. Qui voudrait utiliser un format statique, alors que l’on peut faire un gros tas de code spaghetti dans un script qu’il faut de toute manière garder ? Comment expliquer à celles et ceux qui découvrent le langage qu’il faut faire un fichier Python et un fichier INI, alors qu’on peut techniquement se passer du fichier INI ? Vous avez compris : on ne peut pas lutter contre l’appel du code.
Ceci explique pourquoi setup.cfg
est relativement peu
utilisé aujourd’hui. Attaché aux deux énormes boulets omniprésents que
sont setuptools
et setup.py
, il n’apporte au
fond qu’une petite dose de simplicité par son côté déclaratif. Tant
qu’il transportera avec lui tout l’attirail d’un historique pesant et
sclérosant, il restera un choix de seconde zone, une tentative un peu
maladroite de résoudre un problème réel.
requirements.txt
Voilà un fichier que vous avez sans doute déjà croisé et déjà
utilisé. Vanté sans finesse par les tutoriels de seconde zone, loué
pour sa simplicité et sa puissance, utilisé par bon nombre de projets
renommés, requirements.txt
est la star de l’installation
de dépendances.
Oui mais voilà, disons-le de but en blanc : il n’a rien à voir avec la création de paquets.
requirements.txt
, c’est une simple liste de paquets à
installer, avec la possibilité d’en déterminer les versions, les
sources, les branches et les options d’installation.
Il s’utilise souvent avec pip
et ne sert que pour
l’installation. On peut le voir comme une façon pratique de lister des
dépendances, dans un format que l’on pourrait passer directement en
ligne de commande mais que la flemme et le goût pour les sauts de ligne
nous poussent à confiner dans un fichier.
C’est pratique, en particulier pour ce qu’on souhaite partager sous une
forme différente de celle d’un paquet. Au hasard : tout sauf les
bibliothèques. Un petit script sans prétention ?
Un requirements.txt
. Une application web ?
Un requirements.txt
. Une
bibliothèque ? Bon,
d’accord, quand même des requirements.txt
pour la
documentation et les tests.
Oui, on peut avoir un setup.py
, un setup.cfg
et un requirements.txt
dans le même projet. Avec tous
leurs amis MANIFEST.in
, tox.ini
,
pyproject.toml
, pytest.ini
, et je vous en
passe quelques uns. Tout le monde fait sa petite sauce au petit bonheur
la chance, en repompant allègrement sur ses petits camarades des trucs
qui ont l’air de vaguement fonctionner. On trouvera toujours un cas
particulier qui n’est géré qu’avec l’un de ces fichiers, et on
sacrifiera la simplicité sur l’autel de la sacro-sainte fonctionnalité.
MANIFEST.in
Vous voulez une fonctionnalité bien particulière ? L’inclusion de fichiers annexes dans un paquet source est un bon exemple de casse-tête.
Généralement, quand on distribue un paquet binaire, on le fait pour que les utilisateurs puissent facilement utiliser le code. Les paquets comme les wheels sont des archives prêtes à l’emploi, dont l’installation ne nécessite guère plus qu’une décompression dans le dossier qui va bien. Ces paquets peuvent ne contenir que le strict minimum : le code. Tout ce qui est annexe (la documentation, les tests, les petits-fichiers-super-trognons permettant de décrire les changements…) n’a rien à faire dedans.
C’est différent pour les paquets sources. Ces paquets servent à beaucoup de gens de faire beaucoup de choses : regarder le code, créer des paquets pour les distributions, tester des patchs, installer la bibliothèque, lancer des tests… Alors on tente d’inclure le maximum de choses dans le paquet, presque tout ce qu’on a dans le dépôt, sauf ce qui concerne l’intégration continue, la configuration du gestionnaire de versions, et autres broutilles qui viennent également polluer notre joli projet.
Pour intégrer des fichiers dans le paquet source, en particulier quand
ces fichiers sont à la racine du projet et pas dans le même dossier que
le code, on utilise MANIFEST.in
. Cet énième fichier vient,
comme il se doit,
avec sa propre syntaxe et ses propres commandes.
Et n’ayez crainte : il permet à la fois de faire certaines choses que
les autres fichiers permettent et certaines choses que les autres
fichiers ne permettent pas.
pyproject.toml
On y arrive.
Au premier abord, pyproject.toml
semble tout droit être un
clone de setup.cfg
, avec un format légèrement différent et
un nom discutable. Encore un autre fichier, encore un autre format,
mais quelle idée saugrenue ?
En réalité, les choses sont un peu plus complexes. La PEP 518 qui introduit ce fichier s’appelle, si on tente de traduire son titre, « Spécification des dépendances minimales du système de construction des projets Python ». Ce n’est pas « Encore un nouveau format stupide pour définir les métadonnées de mon paquet » et il y a de bonnes raisons à cela.
Dans la liste des problèmes causés par setuptools
, en
voilà un qui n’a pas encore été abordé : setup.py
contient
les dépendances du paquet, dont les dépendances utilisées pour
construire le paquet. Comment faire pour connaître ces dépendances sans
exécuter le fichier ? Et comment exécuter le fichier sans connaître les
dépendances ? Ce problème de la poule et de l’œuf est problématique
pour setuptools
, mais puisque tout le monde l’utilise pour
faire les paquets et qu’il est en dépendance de pip
, il y
a de grandes chances qu’il soit déjà installé avec Python. Par contre,
si l’on souhaite utiliser un autre outil, comme une extension
à setuptools
, les choses deviennent tout de suite moins
faciles.
L’idée de pyproject.toml
n’est pas de proposer un nouveau
format de métadonnées. L’idée est d’inclure, dans un fichier texte
simple, les dépendances nécessaires pour construire un
paquet. Réfléchissez bien à cela. Encore un peu.
Voilà. Vous avez compris. On va pouvoir se débarrasser
de setuptools
et distutils
, au moins pour
construire des paquets. Pour de bon.
Bien sûr, dans les cas simples, on peut continuer de les
utiliser. pyproject.toml
permet de stocker toutes les
métadonnées que l’on stockait auparavant. Il permet également de
stocker les informations plus complexes, telles que les dépendances et
les versions de Python supportées, un peu comme
dans setup.cfg
, un peu comme avant.
Mais rien n’empêche d’utiliser un autre outil, qui peut définir
lui-même ses options de configuration, indépendantes de celles
de setuptools
. Mieux : le fichier étant spécifié et bien
construit, il laisse la place à tous les autres outils annexes
(black
, pylint
,
coverage
…) d’utiliser eux aussi ce fichier. Et de mettre
fin à l’atroce ensemble de confettis de fichiers de configuration.
Reste une chose à régler : définir le point d’entrée de l’outil que
l’on va utiliser pour créer le paquet. C’est le rôle de
la PEP 517 qui
nous permet de nous affranchir totalement de setuptools
,
de setup.py
et de tous leurs amis.
Mais… Ça marche vraiment ?
Oui. Il ne nous reste qu’à voir quels outils utiliser. Ce sera pour le prochain article…