Stella

L’enfer des paquets Python : l’expression des besoins (6 / 7)

La gestion des paquets Python est parfois un enfer. C’est d’autant plus un enfer que quand on parle de création ou d’installation d’un paquet, on devrait déjà commencer par définir précisément ce qu’on entend par là.

💕💕💕

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 😭.

💕💕💕

Papa, Maman : comment on fait les paquets ?

C’est bien beau de parler de paquets en long et en large, mais on en a parlé souvent de travers. À vrai dire, on a beaucoup parlé de ce qu’il y avait dedans, de ce qu’on pouvait en faire, mais pas dans le détail de comment les différentes étapes de sa vie, dont sa naissance, se déroulaient.

L’idée n’est pas de faire un exposé technique du fonctionnement de setuptools (vous pouvez me remercier). L’idée est plutôt de prendre conscience que la notion de paquet regroupe des réalités très diverses, et qu’on ne devrait pas utiliser les mêmes outils et les mêmes protocoles selon la réalité à laquelle on est confrontée.

Vous avez toujours pensé que créer un paquet, c’était juste mettre les fichiers d’un dossier dans une archive ? Désolé de casser l’ambiance, mais ce n’est pas ça, non. Ce n’est pas ça du tout. Sinon, je ne serais pas là pour en parler, et vous ne seriez pas là pour lire cet article.

Mandatory Related XKCD™
Le XKCD obligatoire. Ces gens qui font des paquets Python sont tous incompétents, donnez-moi 5 minutes et je vous résous le problème une bonne fois pour toutes.

Le théâtre des opérations

On y est. Vous avez fini votre code, vous voulez le diffuser, et pour cela vous voulez créer un paquet. Vous avez lu d’innombrables tutoriels qui vous ont vanté d’innombrables techniques infaillibles, d’innombrables outils formidables, d’innombrables fichiers magiques.

Je vais vous donner mon avis (vous êtes là pour ça, non ?). La bonne question à vous poser, c’est : « quelles opérations devront être effectuées pour créer et installer mon paquet ? ». À partir de là, vous pourrez piocher dans votre bibliothèque le tutoriel le plus adapté à votre situation.

Vous lancer tête baissée dans la création de votre paquet, c’est prendre un double risque : vous prendre la tête à en faire trop, ou vous prendre la tête à ne pas pouvoir en faire assez. Croyez-moi sur parole : vous ne voulez ni l’un, ni l’autre. La vie est trop courte.

Alors, qu’est-ce qui se passe dans nos paquets ?

Le code du module

Évidemment, si vous voulez distribuer un paquet Python, il y a d’immenses chances que vous ayez du code Python à inclure dans votre paquet. Et quand on parle de code, le plus souvent on parle d’un module.

Un module Python, dans son expression la plus simple, c’est un simple fichier, ou un dossier qui contient plusieurs fichiers, et potentiellement des sous-dossiers qui composent autant de sous-modules.

Si l’on met de côté l’installation de scripts exécutables (nous en parlerons plus tard), cette étape est sans surprise particulièrement bien gérée par les différents outils disponibles. Que vous souhaitiez spécifier un dossier et inclure automatiquement le code à l’intérieur, que vous souhaitiez spécifier à la main la liste des fichiers et dossiers à inclure, que vous ayez même plusieurs modules à inclure dans votre paquet, cette étape ne devrait pas poser de problème particulier.

Si vous cherchez vraiment la complication (ça vous a traversé l’esprit, on ne va pas se mentir), vous aurez tout de même la tentation de n’inclure certains fichiers que pour certaines plateformes : un fichier pour Windows, un fichier pour les systèmes l’exploitation normaux. Vous voudrez appliquer quelques modifications au code pour une version de Python, ou selon la présence de certaines dépendances. N’ayez crainte, votre vice maladif ne sera pas mis sous le tapis : nous verrons cela plus tard, lors des opérations à la création et l’installation.

Les métadonnées

Avec votre paquet, peut-être même sans vous en rendre compte, vous voulez trimballer une ribambelle de métadonnées. Trois fois rien, vraiment. Un nom de paquet, un numéro de version, une adresse mail de contact. Et les version de Python supportées, les dépendances, obligatoires et optionnelles. Et une description, des mots-clés, quelques classificateurs, quelques liens normés à mettre sur PyPI vers la documentation et le dépôt source. Et des options spécifiques pour lancer les tests ou pour construire l’aide. Et…

OK. Pas trois fois rien.

Pour notre santé mentale, la plupart de ces métadonnées sont normées et facilement intégrables. Tant que l’on veut intégrer ce qu’il est prévu d’intégrer. Soyez raisonnable. Pas de vagues. Tranquille.

Vous la sentez venir, l’embrouille ?

Vous pourriez avoir la tentation d’intégrer des métadonnées dans des fichiers. Pas des fichiers de configuration, pas des fichiers à intégrer dans le module, des fichiers à mettre à côté. Un README, un CHANGELOG, des trucs comme ça. Un code de conduite, une liste des contributrices et contributeurs, une feuille de route…

Et tout cela, uniquement pour le paquet source. Pour la wheel, on ne veut pas ces fichiers annexes. Évidemment. Cela va de soi.

Rassurez-vous, vous pouvez. Un petit détail, cependant : par défaut, le fichier de configuration qui permet de faire cela est basé sur un pseudo-code comprenant 8 commandes différentes et une syntaxe spécifique d’expressions régulières. Ce fichier ne sert qu’à définir la liste des fichiers à inclure dans le paquet source, il a un nom EN MAJUSCULES, un point, et une extension de deux lettres en minuscules (on ne citera pas son nom). Il a des règles implicites qui dépendent de la version de setuptools, il s’adapte aux dépôts CVS et Subversion (!), il crée automatiquement et obligatoirement des fichiers que l’on ne peut pas changer.

À part ce microscopique détail qui ressemble à s’y méprendre une verrue purulente datant du jurassique, rien à signaler.

Les fichiers annexes du module

Vous faites un correcteur orthographique et vous voulez intégrer des dictionnaires. Vous faites un jeu et vous voulez intégrer des images. Vous faites un simulateur de combats de poulets géants et vous voulez générer des cartes à partir des données géospatiales de Mars. Certes, pourquoi pas.

Image du rover Curiosity sur Mars
Ce n’est pas parce que Curiosity n’a pas trouvé de poulets géants qu’ils n’existent pas.

Le point commun ? Vous voulez intégrer dans votre module des fichiers qui ne sont pas du code Python.

Ces fichiers ne sont pas des métadonnées. Ce sont bien des données à installer avec le code, directement utilisées par le code, et sans lesquelles votre module ne pourrait pas fonctionner. D’ailleurs, sauf si vous souffrez d’une grave carence d’empathie envers le reste de l’espèce humaine, vous mettrez ces données à l’intérieur du dossier de votre module, si possible dans un sous-dossier dédié.

J’ai une bonne nouvelle. Tout ce qui reste assez classique est aisément intégrable. Tout ce qui est à gérer sur mesure peut être géré avec le doigté (et la patience) nécessaire.

Oh, avant de passer à la suite, j’ai une petite chose à vous dire… Pour accéder à ces fichiers depuis votre code, vous aurez sans aucun doute la naïveté d’aller les chercher avec leur nom de fichier, à partir du chemin relatif de votre code. C’est sans compter avec les eggs qui peuvent s’utiliser sans se décompresser, sans compter sur les exécutables Windows que l’on peut construire et qui contiennent toutes les données en un fichier, et sans compter sur les personnes qui viendront vous voir pour vous expliquer que sur Mars, les poulets géants utilisent un format d’archives quantiques qui s’utilisent à la fois compressés et décompressés.

J’exagère à peine. Dans le doute, utilisez importlib.resources. Comment ça, c’est uniquement à partir de Python 3.7 ? Eh bien… Faites preuve d’un peu de résilience, que diable ! Après quelques semaines à lire tous les forums de la Terre vous devriez aisément trouver une solution qui fonctionne dans tous les cas.

Les exécutables

Cette partie est simple : n’intégrez pas d’exécutable.

Bon, d’accord. Vous râlez parce que vous ne comprenez pas comment, sans exécutable, on pourra lancer votre superbe logiciel qui incruste un Nyan Cat sur votre fond d’écran. Et vous avez bien raison.

Nyan Cat
Ce chat qui vole sera du plus bel effet sur votre fond d’écran. Réfléchissez-y sérieusement.

Mais j’ai raison aussi. Pour avoir un exécutable installé par votre paquet, vous n’avez pas besoin de l’écrire. Python vous propose un astucieux système de points d’entrée pour vous économiser une partie du travail.

Ces points d’entrée sont des fonctions qui seront automatiquement transformées en exécutables à l’installation de votre paquet. Cette solution offre pas mal d’avantages, comme celui de pouvoir utiliser votre application backnyan en lançant backnyan dans votre terminal (ou en cliquant sur l’icône de l’exécutable installé), mais également comme module avec python -m backnyan. Et voilà, sans vous en rendre compte, vous avez gagné la possibilité d’utiliser un autre module avec votre application. Au hasard, avec python -m pdb -m backnyan vous pouvez désormais vous adonner aux joies de la correction interactive d’erreurs.

Je vous souhaite bien du plaisir avec pdb. C’est cadeau. Paquet cadeau.

Les opérations à la création

Jusqu’à ce moment précis, je vois dans vos yeux les innocentes lueurs de l’espoir, de celles qui animent les êtres dotés d’une raison de vivre encore immaculée, enivrés par l’alléchante fragrance de l’atteignable réussite.

Je vous propose de vous arrêter là.

Tant pis pour vous, si vous continuez, n’allez pas vous plaindre.

Nous avons vu à maintes reprises que setup.py est un fichier Python classique qui permet d’exécuter toutes sortes de fantaisies. Et par « fantaisies », je ne veux pas parler de licornes, je suis plutôt Cerbère ou Minotaure. Des trucs qui mordent et qui font mal.

Dans les opérations que peut faire setup.py, et si vous le voulez bien, nous déterminerons deux groupes distincts : celles faites avant de créer le paquet, et celles faites après. Nous commencerons ici par le premier groupe.

À la création d’un paquet, on pourrait avoir l’envie déviante de tripatouiller les fichiers. On voudrait à la volée créer des fichiers à inclure dans le paquet, ou en récupérer en ligne. On voudrait faire quelques ajustements pour créer des paquets optimisés pour une version de Python particulière, ou pour un système d’exploitation particulier.

Oh, mais attendez, ça donne des idées. On pourrait compiler du code C pour intégrer des versions différentes dans des wheels spécifiques. On pourrait directement créer des exécutables ou des archives spécifiques. On pourrait obfusquer du code propriétaire.

Au lieu d’écrire un module, on pourrait écrire un méta-module qui génère le code du module à la volée.

Vous avez saisi l’idée.

Si ces exemples vous semblent étranges, voire farfelus, prenez quelques minutes pour y réfléchir sérieusement. Ce ne sont que des exemples tirés de faits réels, mis en place sans mauvaise foi. True story. Et Python permet de faire cela sans trop de peine, puisque setup.py est un simple fichier Python.

Cela montre également que sans fichier setup.py, il serait très difficile d’effectuer tout cela. On ne peut pas faire de simple fichier de configuration qui prend en compte tous ces cas.

On commence à comprendre ici pourquoi il est illusoire en Python d’avoir un outil unique pour créer des paquets. Entre la simplicité et la complexité, entre un format statique et du code dynamique, la bonne solution dépend du contexte. C’est pour cela que nous aurons encore pendant longtemps de nombreux tutoriels, chacun axé sur une solution particulière, sans échappatoire unique possible.

Les opérations à l’installation

Nous voilà arrivés à l’étape ultime.

Même si l’on peut faire beaucoup de choses complexes au moment de la création d’un paquet, parfois pour des raisons honnêtes, cela reste cantonné à la responsabilité de celle ou celui qui crée le paquet. À la limite, toutes ces opérations pourraient être faites en dehors de l’outil de création de paquet, par un script externe exécuté avant d’utiliser la pile logicielle classique pour générer l’archive.

Dit comme ça, c’est presque facile.

La vraie complexité, c’est d’exécuter du code à l’installation. Pour cela, on est (presque) obligé de dépendre de ce que pip fournit, et donc de retomber dans les aléas de setuptools.

Pire. Le code étant exécuté sur la machine où le paquet est installé, il doit s’adapter à ses spécificités : son système d’exploitation, son système de fichiers, sa version de Python, ses outils installés… Il est donc souvent nécessaire de faire preuve d’inventivité et de dextérité pour mettre en place du code qui s’adapte à la cible.

Mais, pour quoi faire ?

Dans le cas de paquets sources, on peut vouloir faire à l’installation à peu près tout ce que l’on voulait faire à la création d’une wheel. Mais on peut cette fois-ci le faire en utilisant tout ce qui est à disposition sur la machine hôte : compilation optimisée selon l’architecture, interfaçage avec des bibliothèques spécifiques installées, adaptation à la version de Python, du système d’exploitation, de certaines dépendances…

setuptools, pour ne citer que lui, offre une bonne dose d’outils pour simplifier ces tâches, et en particulier la compilation. Écrire du code C au milieu de sa bibliothèque Python, pour en optimiser certaines parties, est une pratique assez répandue. Et dans ce cas, soit on a les moyens pour générer des wheels pour toutes les plateformes (utopique), soit on laisse pip se débrouiller à l’installation.

Malheureusement, dépendre de l’hôte signifie dépendre des outils qui y sont installés. Il faut donc espérer que votre cible possède un compilateur en état de marche, adapté à ce que setuptools sait faire. Dans le cas contraire, le travail nécessaire pour installer votre trésor de code risque fort de décourager même les plus téméraires.

On va où, maintenant ?

Maintenant que nous avons entraperçu l’étendue de ce que l’on peut faire avec un paquet Python, nous voilà bien avancés. On va où, maintenant ?

Mine de rien, nous avons bien avancé. Si vous avez bien suivi (et je n’en doute pas), vous devriez pouvoir vous en sortir. Après avoir déterminé plus précisément ce que vous souhaitez et devez faire avec votre code, vous devriez être à même d’exploiter le tableau de la boîte à outils pour choisir avec précision les armes les plus adaptées.

Évidemment, il va vous falloir lire une bonne dose de documentation, d’articles et autres forums pour vous faire une idée plus précise. Si vous faites preuve d’un peu de patience, vous pourrez également lire un énième tutoriel partial, puisque c’est le sujet de notre dernier article.

D’ici là, bonnes lectures…