Stella

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 :

  1. Le sac de nœuds
  2. Les racines du mal
  3. La folie des formats
  4. Des fichiers partout
  5. La boîte à outils
  6. L’expression des besoins
  7. 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 ?)

Mème Toy Story « des fichiers partout »
T’en veux des fichiers ? Tu vas en avoir plein les mirettes !

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.

Mandatory Related XKCD™
Le XKCD obligatoire

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.

Digramme de Venn à 5 ensembles
Voyons voir… Avec quels fichiers je peux déterminer une dépendance optionnelle dynamique qui ne s’installera qu’avec la version 3.7.x de Python sur un Windows 32 bits ?

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