Packager son code python
Les informations et conclusions de cet article sont basées sur mes propres analyses et interprétations, elles ne sont pas entiÚrement fiables. Je vous invite à vous référer aux sources originelles et à me faire vos retour en commentaire.
Encore mieux, n'hĂ©sitez Ă proposer directement des modifications sur Github đ
Packager son code, c'est l'emballer dans un joli paquet afin que d'autres personnes puisse le rĂ©utiliser đ.
Il s'agit d'un concept trĂšs important. A chaque fois que vous faites pip install unpackage
ou import unpackage
,
vous utilisez des paquets que d'autres ont emballé pour vous.
Alors comment ça marche ? Ne pourrait-on pas se contenter de se partager notre code avec git ? Et bien, c'est plus complexe qu'il n'y paraßt. En fait il y a tout un groupe de travail, la Python Packaging Authority (alias pypa), qui bosse dessus depuis 2011.
Dans cette article, on fera un rapide tour d'horizon du packaging en python et on présentera une méthode simple pour packager son code en 2023.
Sommaireâ
Les packages en pythonâ
D'abord quelques définition :
Module pythonâ
Un module python un est fichier texte dont l'extension est .py
et qui contient du code python.
On appelle cela module car python permet d'importer les éléments définis dans les fichiers .py
, ce qui évite de
devoir mettre tout son code dans un seul gros fichier.
Par exemple si l'on crée un fichier count_lines.py
qui contient une fonction count_lines_file
il est ensuite
possible d'importer cette fonction :
def count_lines_file(filepath: str) -> int:
"""Count the number of lines in a file"""
return sum(1 for _ in open(filepath))
from count_lines import count_lines_file
count_lines_file("count_lines.py")
3
Lorsque l'on execute l'instruction from count_lines import count_lines_file
, python cherche le module
dans le rĂ©pertoire courant (lĂ oĂč python a Ă©tĂ© lancĂ©) ; puis dans le rĂ©pertoire d'installation de python
/usr/lib/python3.11
; puis dans le rĂ©pertoire oĂč python installe les packages par dĂ©faut :
/usr/lib/python3.11/site-packages
.
DÚs que le module est trouvé, il est exécuté dans l'environnement python et les éléments qui y sont définis deviennent disponibles.
La liste des répertoires dans lequel python cherche les modules est grùce au package sys
:
import sys
sys.path
["", "/usr/lib/python3.11/python311.zip", "/usr/lib/python3.11/python311", "/usr/lib/python3.11/site-packages"]
Package pythonâ
Un package est un dossier qui regroupe un ensemble de
modules python et qui facilite leur accÚs en créant un espace de nommage :
from numpy.linalg import norm
.
Pour créer un package, il suffit de créer un fichier __init__.py
(qui peut tout a fait ĂȘtre vide) dans un dossier.
Le dossier est alors considéré par python comme un package.
Créons un package pour le module count_lines.py
:
count_package
âââ __init__.py
âââ count_lines.py
A présent on peut utiliser la notation "pointée" pour importer le module count_lines
:
from count_package.count_lines import count_lines_file
count_lines_file("count_lines.py")
3
Lorsque l'on execute l'instruction from count_package.count_lines import count_lines_file
, python cherche un dossier
count_package
contenant un fichier __init__.py
dans les répertoires de sys.path
(le répertoire courant et les
répertoires par défaut vus plus haut).
Si le package est trouvé, les modules qui y sont présents sont accessibles via la notation "pointée".
Distribuer son packageâ
Pour distribuer son package on crĂ©e une distribution. Il s'agit d'une archive contenant le package Ă distribuer et qui pourra ensuite ĂȘtre installĂ© avec le gestionnaire de paquet pip.
Il en existe deux formats de distributions principaux :
Le format Source Distribution (sdist) : il s'agit d'une archive contenant l'ensemble du code source et des métadonnées
Le format Built Distribution : il s'agit d'un format de distribution oĂč un certain nombre de choses ont Ă©tĂ© prĂ©-compilĂ©es pour faciliter l'installation sur d'autres environnement. C'est notamment utile pour les modules Ă©crits en C / C++.
Le format wheel
est le format de type Built Distribution de référence. Il s'agit du format développé par la Python
Packaging Authority et qui est trÚs souvent utilisé pour distribuer les packages.
Le workflow de distribution ressemble schématiquement à ça :
Il existe de nombreux outils pour créer des distributions mais aujourd'hui on se focalisera principalement sur les
outils créés par la Python Packaging Authority et qui sont devenus des incontournables, à savoir setuptools
, build
et twine
.
setuptoolsâ
setuptools
est l'outil utilisé par la grande majorité des projets
pour construire leurs distributions.
Reprenons notre exemple count_package
de tout à l'heure et voyons comment créer une distribution avec setuptools
.
L'arborescence de notre projet pourrait ressembler à ça :
projet_genial
âââ count_package
â âââ __init__.py
â âââ count_lines.py
âââ tests
â âââ test_count_lines.py
âââ .gitignore
âââ LICENSE.md
âââ README.md
Si on ne lui dit pas, setuptools
n'a aucun moyen de savoir quels sont les fichiers et repertoires qui doivent ĂȘtre
inclus dans notre distribution. Il ne peut pas non plus deviner notre adresse mail, le nom de notre projet, sa version,
etc.
setuptools
a donc besoin d'un fichier de configuration qui lui permettra de savoir quoi inclure dans la distribution.
Là on ça devient rigolo c'est qu'il y a trois types de fichiers de configuration possible pour setuptools (et ils sont non mutuellement exclusifs) :
- Le fichier
setup.py
, le plus ancien et le plus populaire (3.7 millions de résultats sur github) - Le fichier
setup.cfg
, le second venu et qui a fait de nombreux et nombreuses adeptes (0.4 millions de résultats sur github) - Le fichier
pyproject.toml
, le dernier venu et qui est à présent le standard officiel pour tous les outils de packaging python. (0.2 millions de résultats sur github)
pyproject.toml
est aujourd'hui le standard officiel mais reste encore trĂšs
minoritaire par rapport Ă setup.py
, c'est pourquoi nous aborderons les trois formats de fichier.
setup.pyâ
Le fichier setup.py
comme son extension le laisse penser est un fichier python. Il a la forme suivante :
from setuptools import setup
setup(
name='count_package',
author='me',
description='Package for counting the number of lines in files.'
version='0.0.1',
python_requires='>=3.7, <4',
install_requires=[
'pandas',
'importlib-metadata; python_version >= "3.8"',
],
)
Le fait qu'il s'agisse d'un fichier python fait à la fois sa force et sa faiblesse : il est possible de construire la configuration dynamiquement dans le code mais ça rend la configuration difficile à parser et à interfacer avec d'autres outils externes.
Puisqu'il s'agit d'un format propre Ă setuptools
les distributions de type sdist ne seront installables que si
setuptools a bien été installé sur l'environnement cible et dans une version compatible.
De plus puisque l'immense majorité des projets se sont mis à utiliser setuptools et setup.py
il est devenu difficile
de proposer des alternatives et des projets comme flit on dĂ» ĂȘtre construit "par
dessus" setuptools
, ce qui ne favorise pas l'innovation.
Par ailleurs son utilisation est souvent problématique. Pour reprendre l'exemple de
cet article, vous pourriez par exemple ĂȘtre tentĂ© de
d'introduire une condition if/else dans votre setup.py
pour gérer une dépendance nécessaire en python 2.7 en vous
basant sur sys.version
mais ce faisant vous introduiriez un bug vicieux : la dépendance sera embarquée ou non en
fonction de l'environnement qui compile la distribution de votre package et non pas en fonction de l'environnement de
l'utilisateur qui l'installe.
Il est aussi tentant
d'importer le package depuis setup.py
pour gérer la version sa version par exemple. Ce qui fera planter les distributions sdist.
Aujourd'hui l'utilisation de setup.py
est possible mais il est conseillé de privilégier une utilisation déclarative.
De plus l'utilisation du fichier comme script : python setup.py
est déprécié comme l'indique la documentation de
setuptools :
It is important to remember, however, that running this file as a script (e.g. python setup.py sdist) is strongly discouraged, and that the majority of the command line interfaces are (or will be) deprecated (e.g. python setup.py install, python setup.py bdist_wininst, âŠ).
We also recommend users to expose as much as possible configuration in a more declarative way via the pyproject.toml or setup.cfg, and keep the setup.py minimal with only the dynamic parts (or even omit it completely if applicable).
See Why you shouldnât invoke setup.py directly for more background.
setup.cfgâ
Pour répondre aux problÚmes mentionnés ci-dessus et rendre la configuration plus déclarative, la pypa a créé en 2016 le
format de fichier setup.cfg
.
Le fichier setup.py
d'exemple plut haut est équivalant au fichier setup.cfg
suivant :
[metadata]
name = count_package
version = 0.0.1
author = me
description = Package for counting the number of lines in files.
[options]
python_requires = >=3.7,<4
install_requires =
pandas
importlib-metadata; python_version >= "3.8"
Ce format a fait de
nombreuses et nombreux adeptes mais a
récemment été supplanté par le format pyproject.toml
qui est dorénavant le moyen officiel de déclarer la configuration
des packages python.
pyproject.tomlâ
Le format pyproject.toml
en plus de reprendre l'approche déclarative de setup.cfg
, introduit sont lot de nouveauté.
Il est dĂ©sormais possible (et mĂȘme obligatoire) de spĂ©cifier le builder du package. Il est Ă©galement un moyen de
centraliser les configuration de nombreux outils de développement de façon agnostique plutÎt que de multiplier les
fichiers de configuration du type tox.ini
, .coveragerc
, etc.
A propos du builder : comme je le faisais remarquer plus haut, lorsqu'un package est distribué sous format sdist, c'est
le client qui doit construire le package. Lorsqu'il exécute la commande pip install count_package
, pip doit donc
récupérer la distribution sdit, puis construire le package à partir de la configuration qu'il s'y trouve.
Comment pip fait-il pour savoir quelle configuration utiliser ? Et comment utiliser un autre builder que setuptools
Ă ce moment lĂ ?
Pour répondre à cette problématique, le format pyproject.toml
inclus une section obligatoire pour définir le builder
qui doit ĂȘtre utilisĂ© pour construire le package :
[build-system]
requires = [
"setuptools>=44",
"wheel>=0.30.0",
"cython>=0.29.4",
]
build-backend = "setuptools.build_meta"
Avec pyproject.toml
il est désormais possible de déclarer à pip
les dépendances nécessaire au build !
Il est alors tout à fait possible de spécifier l'utilisation d'un autre builder que setuptools, tel que flit :
[build-system]
requires = ["flit"]
build-backend = "flit.api:main"
Ce format est en train de s'imposer petit à petit comme le moyen de centraliser la configuration du package. Il est le format privilégié par setuptools et un certains nombres d'outils tiers l'utilise pour stocker leur configuration : black, pytest, isort, etc.
Voici un exemple de fichier pyproject.toml
pour notre package d'exemple count_package
:
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "count_package"
version = "0.0.1"
description = "Package for counting the number of lines in files."
name = "my_package"
authors = [
{name = "me", email = "email@me.fr"},
]
requires-python = ">=3.8,<4"
dependencies = [
"pandas",
'importlib-metadata; python_version >= "3.8"',
]
dynamic = ["version"]
La documentation de setuptools donne plus de
détails la configuration au format pyproject.toml
Construire une distribution de son packageâ
Une fois le fichier de configuration (de préférence pyproject.toml
) écrit, il ne vous reste plus qu'à construire votre
package.
Pour ça, le moyen moderne de procéder et d'utiliser le package build
développé par
la pypa :
pip install --upgrade build
python -m build
build
commencera par installer le builder spécifié dans votre fichier pyproject.toml
puis l'utilisera pour
construire une distribution sdist et une wheel.
Vous pourrez ensuite utiliser le package twine
pour le publier sur le
repository officiel pypi.
Pour des raisons de rétro-compatibilité avec les anciennes versions des librairies de packaging,
vous pouvez créer un fichier setup.py
minimal
en plus du fichier pyproject.toml
:
from setuptools import setup
setup()
Gestion de la versionâ
Dans l'exemple de pyproject.toml
vu plus haut la version du package est fixée manuellement. Il faut donc changer
celle-ci Ă chaque fois que l'on souhaite publier une nouvelle version de son package.
Contrairement Ă poetry setuptools n'a pas (Ă ma connaissance) de commande
intégrée pour modifier facilement la version du package dans le pyproject.toml
. En revanche la pypa propose un
package que je trouve trÚs pratique pour gérer la version du package en s'appuyant sur git ou mercurial :
setuptools-scm
Cela s'utilise trÚs simplement en ajoutant la dépendance au pyproject.toml
ainsi qu'en spécifiant que la version est
dynamique :
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
[project]
# version = "0.0.1" # Remove any existing version parameter.
dynamic = ["version"]
[tool.setuptools_scm]
write_to = "src/pkg/_version.py"
Lors de la construction du package setuptools-scm
recherchera parmi le dernier tag ayant un numéro de version valide
puis en déduira le numéro de version du package. Par défaut la version est construite à partir de trois éléments :
- Le dernier tag ayant un numéro de version valide (exemple :
v1.2.3
) - La distance à ce tag (nombre de révision depuis ce tag)
- L'état du répertoire de travail (s'il y a des changements non commités)
Une fois le numéro de version déduit, un fichier _version.py
sera créé à l'intérieur de la distribution à l'endroit
spécifié (ex : src/pkg/_version.py
) et qui permettra de connaĂźtre la version du package Ă partir de la distribution
sans que l'historique git ne soit présent sur l'environnement cible.
Si vous avez l'habitude de renseigner vous mĂȘme le numĂ©ro de version de vos packages sachez que les formats valides pour les versions sont rĂ©gis par la PEP 440. Si vous ne respectez pas ces spĂ©cifications vous risquez probablement d'avoir des problĂšmes lors de la publication ou de l'installation de vos packages.
Notamment les versions v1.2.3-local
ou v1.2.3-dev
sont invalides.
Le layoutâ
Lors de la configuration de votre package, peut importe la méthode utilisée (setup.py
, setup.cfg
ou
pyproject.toml
), vous devez spécifier les packages et les sous-packages que vous souhaitez inclure dans votre
distribution :
[tool.setuptools]
packages = ["mypkg", "mypkg.subpkg1", "mypkg.subpkg2"]
Heureusement setuptools
possĂšde une
fonctionnalité de découverte automatique de vos
packages et sous-packages. Celle-ci est compatible avec deux layout projets classique :
le layout dit Ă plat (flat-layout) :
count_package
âââ count_package
â âââ __init__.py
â âââ count_lines.py
âââ tests
â âââ test_count_lines.py
âââ .gitignore
âââ LICENSE.md
âââ README.md
et le layout avec un dossier src (src-layout) :
projet_genial
âââ src
| âââ count_package
â âââ __init__.py
â âââ count_lines.py
âââ tests
â âââ test_count_lines.py
âââ .gitignore
âââ LICENSE.md
âââ README.md
La diffĂ©rence peut paraĂźtre minime mais personnellement j'ai une grosse prĂ©fĂ©rence pour le src-layout car cela empĂȘche de prendre des mauvaises habitudes et cela force Ă comprendre comment le systĂšme d'import et d'installation de package fonctionne en python.
En effet, lorsque vous développez votre package vous tester au fur et à mesure les fonctionnalités que vous ajoutez à celui-ci.
Pour ce faire, on est trÚs tenté de simplement importer notre package depuis notre fichier de test :
from count_package import count_lines
Cela fonctionnera si vous utilisez un flat-layout et que vous exécuter votre module de test depuis le répertoire qui
contient le dossier count_package
car comme on l'a vu plus haut python inclus le répertoire courant dans a liste des
rĂ©pertoire oĂč il recherche les modules.
Cependant c'est une mauvaise habitude pour deux raisons :
- D'abord, si vous utilisez setup.py, celui-ci se trouvant à la racine de votre projet, il est en capacité d'importer le
package
count_package
qu'il est censé installé chez un client en mode sdist ce qui peut provoquer des bugs si on ne fait pas attention. - Ensuite, vous ne testez pas vraiment le package tel qu'il sera installé chez les autres ! En effet, il se peut par exemple que vous n'ayez pas pensé à inclure des fichiers de données dans votre fichier de configuration et vos tests devraient en conséquence planter. Mais comme ces fichiers sont présents chez vous, vous vous ne apercevez de rien.
Pour ces raisons, je pense qu'il est préférable d'opter pour un src-layout et d'utiliser une
installation editable grĂące Ă la commande :
pip install -e .
pour le développement local. Cela permet d'installer le package en faisant un lien symbolique avec
votre code afin que les modifications que vous y apportez soient immédiatement répercutées sur le package installé.
Pour une analyse approfondie des avantages du src-layout, je vous renvoie Ă cet article (qui date de 2014).
Les Alernatives Ă setuptoolsâ
Il existe aujourd'hui des alternatives solides Ă setuptools, en voici une liste non exhaustive :
- Flit : met l'accent sur la simplicité d'utilisation et de configuration.
- Pipenv : permet de gérer conjointement l'environnement virtuel de son projet et
ses dépendances. Ajoute de plus une fonctionnalité trÚs appréciable : la génération de fichiers
Pipfile.lock
qui référence des versions exacte des dépendances pour permettre la reproduction à l'identique de l'environnement de développement. - Poetry : outil trÚs puissant qui permet à la fois de gérer les environnements
virtuels, les dépendances (et les dépendances des dépendances), de générer un fichier
poetry.lock
similaire ĂPipfile.lock
, de publier son package, etc.
Je trouve pour ma part que Poetry est l'outil le plus prometteur au vu de sa capacité à résoudre les conflits entre les dépendances.
TL;DR Packager son code python en 2023â
Mes conseils pour packager votre code en 2023 :
- Utilisez le fichier pyproject.toml en suivant le guide setuptools.
- Un petit coup de
pip install build && python -m build
et votre package est prĂȘt Ă ĂȘtre distribuĂ©. - Adoptez le src-layout.
En bonus :
- Essayez Poetry.
- Utilisez ce template
cookie-cutter
tout simple (ou créez le votre) pour vos projets.
Estimons nous heureuses et heureuxâ
Tout ce que je vous ai raconté ici peut sembler beaucoup et pourtant je n'ai fait que survoler le sujet. En tout cas je pense que l'on peut s'estimer heureuses et heureux lorsque l'on voit ce qu'écrivait le site Sam & Max en 2018 :
D'abord on a
distutils
,setuptools
,distribute
, anddistribute2
qui ont tous été à un moment les "standards" recommandés pour packager une lib. Ensuite on a eu l'époque deseggs
,exe
, et autres trucs queeasy_install
allait chercher n'importe oĂč dans la nature en suivant aveuglĂ©ment des liens sur PyPi. Sans compter les machins qu'il fallait compiler Ă tout bout de champ. Et puis rien n'Ă©tait chiffrĂ© au download,pip
n'était pas packagé avec Python, il crevait sur des erreurs stupides type encodage mal géré...à ça se rajoute que
virtualenv
était un truc à part, avec plein de concurrents, et linkait les packages systÚme par défaut. Sans oublier qu'on avait paspython -m
.Bref, le packaging Python, ça a été vraiment la merde. Avec en plus une doc de merde.
C'est quand mĂȘme beaucoup plus simple aujourd'hui.
RĂ©fĂ©rencesâ
- Le Guide de pypa sur le packaging python : An Overview of Packaging for Python
- Un article pour comprendre les wheels : What Are Python Wheels and Why Should You Care?
- Une série de trois articles trÚs éclairant sur le fonctionnement du packaging python, écrit par Bernåt Gåbor en 2019 :
- Un article Ă la gloire de
setup.cfg
sur le regretté site Sam & Max : à propos de setup.cfg - Question stackoverflow : What is pyproject.toml file for