Skip to article frontmatterSkip to article content

Où on va voir que :

Complément - niveau intermédiaire

Vous venez d’écrire un super algorithme qui simule le climat de l’an 2100, et vous voulez le publier ? Nous allons voir ici comment organiser les sources de votre projet, pour que ce soit à la fois

Les infrastructures

En 2024, on ne travaille plus tout seul dans son coin ; il est à la portée de tous d’utiliser et de tirer profit d’infrastructures, ouvertes et gratuites (pour les usages de base au moins) :

Pour ce qui nous concerne ici, voici celles qui vont nous être utiles :

Optionnellement, sachez qu’il existe également des infrastructures pour les deux grandes tâches que sont la documentation et le test, souvent considérées - à tort - comme annexes :

S’agissant de ces deux derniers points : souvent on s’arrange pour que tout soit automatique ; quand tout est en place, il suffit de pousser un nouveau commit auprès de github (par exemple) pour que

Alors bon bien sûr ça c’est le monde idéal ; on ne passe pas d’un seul coup, d’un bout de code qui tient dans un seul module bidule.py, à un projet qui utilise tout ceci ; et bien entendu, aucun de ces trucs n’est obligatoire, on n’a pas forcément besoin non plus d’utiliser toutes ces ressources.

Aussi nous allons commencer par le commencement.

Le commencement : créer un package

Vous pouvez voir ici un repo git qui contient le mini package dont on parle ici.

Le commencement, ça consiste à se préparer à coexister avec d’autres librairies.

Si votre code expose disons une classe Machine dans le fichier/module machine.py, la première chose consiste à trouver un nom unique ; rien ne vous permet de penser qu’il n’y a pas une autre bibliothèque qui expose aussi un module qui s’appelle machine (il y a même fort à parier qu’il y en a plein !). Aussi ce qu’on va commencer par faire c’est d’installer tout notre code dans un package.

Concrètement ça va signifier mettre notre code dans un sous-dossier, mais surtout d’un point de vue des utilisateurs potentiels de la classe, ça veut dire qu’au lieu de faire juste :

from machine import Machine

on va décider qu’à partir de maintenant il faut toujours faire

from bidule.machine import Machine

et de cette façon, tous les noms qui sont propres à notre code ne sont accessibles que via l’espace de noms (du module) bidule, ainsi on évite les conflits avec d’autres bibliothèques.

Choisir le nom du package

Bien sûr ceci ne fonctionne que si je peux être sûr que bidule est à moi, de sorte que personne demain ne publie une librairie qui utilise le même nom.

C’est pourquoi on recommande, à ce stade, de s’assurer de prendre un nom qui n’est pas déjà pris ; en toute rigueur c’est optionnel, tant que vous ne prévoyez pas de publier votre appli sur pypi (car bien sûr c’est optionnel de publier sur pypi), mais ça coûte moins cher de le faire très tôt, ça évite des renommages fastidieux plus tard.

Donc pour s’assurer de cela, on va tout simplement demander à pypi, qui va jouer le rôle de registrar, et nous garantir l’exclusivité de ce nom. Je vous invite à chercher votre nom directement dans le site pypi pour vous en assurer (à noter que pip search bidule n’est plus disponible depuis la ligne de commande)

Le nom est libre, pour toute la suite je choisis bidule comme mon nom de package.

Adapter son code

Une fois que j’ai choisi mon nom de package, donc ici bidule, je dois :

  1. mettre tout mon code dans un dossier qui s’appelle bidule,

  2. et modifier mes importations ; maintenant j’importe tout au travers du seul package bidule.

Donc je remplace les importations partout ; ce qui avant aurait été simplement

from machine import Machine

devient

from bidule.machine import Machine

C’est tout cassé

À ce stade précisément, vous constatez... que plus rien ne marche !

En effet, comme on l’a vu dans le complément sur le chargement des modules, Python recherche vos modules dans l’ordre

Et donc si vous m’avez suivi, vous devez avoir quelque chose comme

mon-depot-git/
             bidule/
                    main.py
                    machine.py
                    helpers.py

mais alors quand vous faites

$ python bidule/main.py
Traceback (most recent call last):
  File "bidule/main.py", line 1, in <module>
    from bidule.machine import Machine
ModuleNotFoundError: No module named 'bidule'

on va chercher du coup un module bidule à partir du répertoire du point d’entrée qui est déjà le dossier bidule/, donc on ne trouve pas.

Le mauvais réflexe

Du coup naturellement, on se dit, ça n’est pas grave, je vais tirer profit de la variable PYTHONPATH. Alors disons-le tout net : ce n’est pas une bonne idée, ce n’est pas du tout pour ce genre de cas qu’elle a été prévue.

Le fait de modifier une variable d’environnement est un processus tarabiscoté, même sans parler de Windows, et cette approche est une bonne façon de se tirer une balle dans le pied ; un jour ou l’autre la variable ne sera pas positionnée comme il faut, c’est sûr.

Bref, il ne faut pas faire comme ça !!

Le bon réflexe : pip install -e .

Non, le bon reflexe ici c’est d’écrire un fichier pyproject.toml, et de l’utiliser pour faire ce qu’on pourrait appeler une installation en mode développeur. Voyons cela :

Je commence donc par créer un fichier pyproject.toml à la racine de mon dépôt git, dans lequel je mets, pour commencer, le minimum :

[project]
name = "bidule"
version = "0.1.0"

Installation en mode developpeur : pip install -e .

Avec ce fichier en place, et toujours à la racine de mon dépôt, je peux maintenant faire la formule magique :

$ pip install -e .
pip install -e .
Obtaining file:///le/chemin/ou/je/suis
<snip>
Successfully built bidule
Installing collected packages: bidule
Successfully installed bidule-0.1.0

L’effet de cette commande est de modifier mon environnement pour que le répertoire courant (le . dans pip install -e .) soit utilisé pour la recherche des modules. Ça signifie que je peux maintenant lancer mon programme sans souci :

$ python 
>>> from bidule import Machine
>>> Machine()
... déroulement normal
>>>

Un pyproject.toml plus raisonnable

Au delà de cette première utilité, pyproject.toml sert à configurer plein d’aspects de votre application ; lorsque votre projet va gagner en maturité, il sera utilisé pour préparer le packaging, pour uploader le package, et au moment d’installer (comme on vient de le voir).

Du coup en pratique, les besoins s’accumulent au fur et à mesure de l’avancement du projet, et on met de plus en plus d’informations dans pyproject.toml; voici quelques ajouts très fréquents que j’essaie de mettre dans l’ordre chronologique reportez-vous à la doc pour une liste complète :

Packager un point d’entrée

Assez fréquemment on package des librairies ; dans ce cas on se soucie d’installer uniquement des modules Python.

Mais imaginez maintenant que votre package contient aussi un point d’entrée - c’est-à-dire en fin de compte une commande que vos utilisateurs vont vouloir lancer depuis le terminal. Ce cas de figure change un peu la donne; il faut maintenant installer des choses à d’autres endroits du système (pensez par exemple, sur linux/macos, à quelque chose comme /usr/bin).

Dans ce cas surtout n’essayez pas de le faire vous-même, c’est beaucoup trop compliqué à faire correctement !

Pour illustrer la bonne façon de faire dans ce cas, je vous renvoie pour les détails à notre exemple réel, mais pour l’essentiel il vous suffit d’ajouter dans pyproject.toml une nouvelle section:

[project.scripts]
# l'installation va créer une commande 'bidule-cli' qu'on pourra appeler dans le terminal
# et qui lancera la fonction 'main()' du fichier bidule/main.py
bidule-cli = "bidule.main:main"

On a dit plus haut qu’on n’avait plus besoin de refaire un pip install, mais dans ce cas précis c’est nécessaire tout de même:

$ pip install -e .
$ bidule-cli
ici le déroulement de la fonction `main()` dans le fichier `bidule/main.py`

Packages et __init__.py

Historiquement avant la version 3.3 pour qu’un dossier se comporte comme un package il était obligatoire d’y créer un fichier de nom __init__.py - même vide au besoin.

Ce n’est plus le cas depuis cette version. Toutefois, il peut s’avérer utile de créer ce fichier, et si vous lisez du code vous le trouverez très fréquemment.

L’intérêt de ce fichier est de pouvoir agir sur :

Un usage particulièrement fréquent consiste à “remonter” au niveau du package les symboles définis dans les sous-modules. Voyons ça sur un exemple.

Dans notre dépôt de démonstration, nous avons une classe Machine définie dans le module bidule.machine. Donc de l’extérieur pour me servir de cette classe je dois faire

# dans le code utilisateur

from bidule.machine import Machine

C’est très bien, mais dès que le contenu va grossir, je vais couper mon code en de plus en plus de modules. Ce n’est pas tellement aux utilisateurs de devoir suivre ce genre de détails. Donc si je veux pouvoir changer mon découpage interne sans impacter les utilisateurs, je vais vouloir qu’on puisse faire plutôt, simplement

# dans le code utilisateur

from bidule import Machine

et pour y arriver, il me suffit d’ajouter cette ligne dans le __init__.py du package bidule :

# dans notre __init__.py
from .machine import Machine 

qui du coup, si vous avez bien suivi la leçon sur les imports, va définir le symbole Machine .. directement dans l’objet package ! c’est ce qu’on voulait :)

Exposer le numéro de version

Nous avons défini le numéro de version dans pyproject.toml; comment faire à présent pour exposer cette information aux utilisateurs de la librairie ?
c’est-à-dire qu’on aimerait pouvoir faire

import bidule
print(bidule.__version__)

pour retrouver notre 0.1.0

Pour y parvenir, on peut - par exemple - procéder comme ceci:

et maintenant:

$ python -c "import bidule; print(bidule.__version__)"
0.1.0

Publier sur PyPI

Pour publier votre application sur PyPI, rien de plus simple :

Ensuite à chaque version, une fois que les tests sont passés et tout :

Signalons enfin qu’il existe une infra PyPI “de test” sur https://test.pypi.org utile quand on ne veut pas polluer l’index officiel.

Utiliser pip pour installer

Ensuite une fois que c’est fait, le monde entier peut profiter de votre magnifique contribution en faisant bien sûr pip install bidule

Remarquez que l’on conseille parfois, pour éviter d’éventuels soucis de divergence entre les commandes python/python3 et pip/pip3,

D’autres formes utiles de pip :

Autres usages de pyproject.toml

Naturellement il y a plein d’autres réglages possibles dans le fichier pyproject.toml

Enfin il s’agit d’un format ouvert, et qu’ainsi on peut y ranger au même endroit des réglages utilisés par d’autres outils complètement. C’est donc là qu’on va trouver les réglages de tous les outils autour de Python per se (builders, linters, ...)