Vous vous êtes déjà demandé comment des projets comme NumPy ou pandas sont organisés en interne ? Comment des centaines de développeurs arrivent à travailler ensemble sans que tout s’écroule ? La réponse commence par une bonne structure de package.
Dans cette première partie, on va poser les fondations: on va écrire du code, le structurer, comprendre ce qui se passe quand on fait import, et construire notre premier package avec pyproject.toml.
Bref, on va voir comment passer d’un simple script Python à une application ayant une bonne structure.
Dans cette partie, nous allons voir comment se structure une application Python en donnant un exemple d’arborescence. Celle-ci n’est pas figée mais est utilisée dans la plupart des projets en calcul scientifique (numpy, pandas, scikit-learn) et autres.
Une application Python est constituée
de fichiers Python avec l’extension
.pyque l’on appelle des modules,de répertoires contenant des fichiers Python que l’on appelle des packages.
Les modules et les packages peuvent être utilisés dans l’interpréteur Python en utilisant la commande import. Lorsque l’on crée sa propre application, il est important de bien comprendre leur fonctionnement afin de définir les comportements que l’on souhaite pour l’utilisateur final lorsqu’il importe l’ensemble ou une partie du package.
Un module¶
Afin d’illustrer l’utilisation d’un module en Python, nous allons écrire un calculateur qui sait uniquement faire une addition et une soustraction.
Voici le fichier calculator_mod.py.
%%file calculator_mod.py
"""
Calculator module
"""
def add(a, b):
"""
return a + b
"""
return a + b
def sub(a, b):
"""
return a - b
"""
return a - bOverwriting calculator_mod.py
Utilisation d’un module¶
Il existe différentes manières d’importer un module en utilisant le mot-clef import.
On peut importer un module via son nom.
import calculator_mod
calculator_mod.add(1, 2)3On peut importer une partie d’un module.
from calculator_mod import sub
sub(1, 2)-1On peut importer un module en modifiant son nom d’appel.
import calculator_mod as calc
calc.add(1, 2)3On peut importer l’ensemble du module.
from calculator_mod import *
add(1, 2)3import définit explicitement certains attributs du module
__dict__: dictionnaire utilisé par le module pour l’espace de noms des attributs__name__: nom du module__file__: fichier du module__doc__: documentation du module
print('file', calculator_mod.__file__)
print('name', calculator_mod.__name__)
print('doc', calculator_mod.__doc__)file /Users/loic/Formations/packaging/practical_session/calculator_mod.py
name calculator_mod
doc
Calculator module
Exécution d’un module¶
On peut ajouter à la fin d’un module le test suivant:
if __name__ == '__main__':
print(add(1, 2))On peut à présent exécuter le module.
%%file calculator_mod.py
"""
Calculator module
"""
def add(a, b):
"""
return a + b
"""
return a + b
def sub(a, b):
"""
return a - b
"""
return a - b
if __name__ == '__main__':
print(add(1, 2))Overwriting calculator_mod.py
! python calculator_mod.py3
Maintenant qu’on sait créer et utiliser un module simple, passons à l’étape suivante. Dans un vrai projet, vous n’allez pas mettre tout votre code dans un seul fichier — ça deviendrait vite illisible. C’est là que les packages entrent en jeu : ils permettent d’organiser votre code en plusieurs modules, comme on range des dossiers dans un classeur.
Un package¶
Comme dit en introduction, un package est un ensemble de modules Python. Prenons l’arborescence suivante
! tree examples/simple_calculator/examples/simple_calculator/
├── calculator
│ ├── __init__.py
│ └── operator
│ ├── __init__.py
│ ├── add.py
│ └── sub.py
└── pyproject.toml
3 directories, 5 files
On trouve ici un package appelé calculator et un sous-package appelé operator dans lequel se trouvent deux modules (add.py et sub.py).
Le fichier __init__.py¶
Prenons l’exemple du fichier calculator/operator/__init__.py
__all__ = ['add', 'sub']De cette manière, on peut importer add et sub en faisant tout simplement
import sys
sys.path.append("./examples/simple_calculator/")from calculator.operator import *On accède ensuite aux attributs et aux fonctions en faisant
print(add.add(1, 2))
print(sub.sub(1, 2))Il est également important d’utiliser l’import relatif lorsque l’on utilise des fonctionnalités de notre application dans les différents modules.
Prenons l’example du fichier calculator/__init__.py
from . import operator
from .operator import *
from .operator.add import addOn a alors le comportement suivant
import calculator
calculator.add(1, 2)calculator.sub.sub(2,3)Solution to Exercise 1
non
oui
non
oui
Vous avez maintenant une bonne idée de ce qu’est un package et de comment import se comporte. Mais il y a une question sous-jacente qu’on n’a pas encore posée : comment Python sait-il où trouver les modules ?
Recherche de modules et de packages¶
Pour que Python importe correctement un module, celui-ci doit être dans son PATH. Le module sys permet de connaître la liste des répertoires où Python va rechercher les modules.
import sys
print(sys.path)Python va donc rechercher dans
le répertoire courant
dans PYTHONPATH si défini (c’est la même syntaxe que le PATH)
dans un répertoire par défaut
On peut également rajouter des répertoires à l’exécution étant donné que sys.path n’est qu’une liste.
sys.path.append("/home/loic/Formations/")
print(sys.path)Lorsque l’on veut importer foo, voici l’ordre des fichiers recherchés dans sys.path.
foo.dll, foo.dylib ou foo.so
foo.py
foo.pyc
foo/__init__.py
À ce stade, vous comprenez ce qu’est un module, un package, et comment Python résout les imports. Mais pour l’instant, votre code n’est utilisable que par vous, sur votre machine. Le vrai enjeu, c’est de le rendre distribuable : que n’importe qui puisse l’installer avec un simple pip install. C’est ce qu’on attaque maintenant.
Réaliser sa première distribution¶
Le contenu à diffuser peut être de différents types
des modules et des sous packages
des données
des scripts
des dépendances
Il est nécessaire d’ajouter un ensemble de fichiers pour pouvoir faire le packaging.
Les indispensables
pyproject.tomlREADME.rstouREADME.mdLICENSE.txtvotre
package
Les optionnels
MANIFEST.in
Voici une arborescence classique d’un package Python
package/
doc/
examples/
package/
...
tests/
tests/
LICENSE.txt
README.rst
pyproject.tomlPremier exemple de pyproject.toml¶
Ce fichier est l’élément central de votre application. Il va non seulement décrire la façon de construire le package, de le définir ainsi que ses dépendances à l’installation mais il va également être utilisé pour la configuration de tous les outils qui vont vérifier la qualité de l’application (black, ruff, pytest, ...).
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "calculator"
dynamic = ["version"]
requires-python = ">=3.8"
[tool.setuptools]
packages = ["calculator"]
[tool.setuptools.dynamic]
version = { attr = "calculator.version.__version__" }build-systemindique les outils pour construire le package ainsi que le backend utilisé. Nous utiliserons tout au long de cet ateliersetuptoolsmais il en existe bien d’autres[1] (Hatch, PDM, Flit, Whey, ...).
projectdéfinit les caractéristiques de votre package (son nom, sa version, ses dépendances, sa description, ses auteurs, ...).
Pour installer votre package, il suffit de faire
pip install .pour installer le package dans le répertoiresite-packagesoù se trouve votre version de Python.pip install . --userpour l’installer dans votre$HOME.pip install -e .pour installer votre application en mode développement. Chaque fois que vous faites une modification, le package est automatiquement mis à jour (ce n’est en fait qu’un lien symbolique vers votre package).
Gérer les dépendances¶
Les dépendances sont gérées à travers le fichier pyproject.toml.
pixi est en mesure de mettre à jour ce fichier lorsque vous utilisez la ligne de commande.
Supposons par exemple que votre package a une dépendance à NumPy pour que celui-ci fonctionne. Il vous suffit de faire
pixi add numpyet vous verrez dans le fichier pyproject.toml
[tool.pixi.dependencies]
numpy = ">=2.0.0,<2.1"Vous pouvez également l’ajouter à la main de la manière suivante
[project]
...
dependencies = [
"numpy",
]De la même manière que ce qu’on a vu pour la définition des environnements de développement, il est possible de spécifier les versions que l’on souhaite.
[project]
...
dependencies = [
"numpy>=1.0,<1.13",
]Il est également possible d’ajouter des dépendances optionnelles comme par exemple
[project.optional-dependencies]
test = ["pytest"]
doc = ["sphinx", "nbsphinx"]On peut ensuite sélectionner ces options lors de l’installation
pip install .[test]
pip install .[test,doc]Mais on préférera passer par pixi en faisant l’équivalent
pixi add -f test pytest
pixi add -f doc sphinx nbsphinxce qui ajoutera les sections suivantes dans le fichier pyproject.toml
[tool.pixi.feature.test.dependencies]
pytest = ">=7.2.0,<7.3"
[tool.pixi.feature.doc.dependencies]
sphinx = ">=7.3.7,<7.4"
nbsphinx = ">=0.9.4,<0.10"Il est ensuite possible de créer des environnements spécifiques à partir des features à l’aide de la commmande
pixi project environment add test -f test
pixi project environment add doc -f docCe qui aura comme conséquence d’ajouter les lignes suivantes dans le fichier pyproject.toml
[tool.pixi.environments]
test = ["test"]
doc = ["doc"]Pour utiliser l’environnement, il suffit de le spécifier via l’option -e comme par exemple
pixi run -e test pytestNous reverrons les environnements dans les prochains chapitres lorsque nous parlerons de la documentation et des tests.
Ajout de scripts¶
En plus des sous packages et modules Python, votre application peut avoir des scripts que l’on peut vouloir exécuter en ligne de commande. Ce sont là encore des fichier Python. Ils sont spécifiés dans le fichier pyproject.toml.
[project.scripts]
calculator-script = "calculator.command_line:main"où calculator-script est le nom de l’exécutable.
Ajouter une extension¶
Un package ne contient pas forcément uniquement des fichiers Python. On peut par exemple avoir des extensions écrites en cython. Ce n’est pas si simple de les prendre en compte avec le fichier pyproject.toml. Si vous souhaitez faire quelque chose de propre, il faut regarder du coté de NumPy ou de pandas. Ils utilisent tous les deux le backend meson pour construire leurs extensions Cython.
Afin de ne pas complexifier l’atelier, nous utiliserons l’ancienne méthode qui se conjugue avec le fichier pyproject.toml. Pour ce faire, nous allons créer le fichier setup.py avec le contenu suivant
from setuptools import setup, find_packages
from setuptools.extension import Extension
from Cython.Build import cythonize
extension = [Extension(name = "calculator.cython_mod",
sources = ["calculator/cython_mod.pyx"])
]
setup(
...
ext_modules=cythonize(extension),
...
)Il faut également ajouter cython dans les dépendances du build-system.
[build-system]
requires = ["setuptools", "cython"]
build-backend = "setuptools.build_meta"! tree TPs/1.packaging/step0/Etape 2¶
Etape 3¶
Ce qu’on a accompli¶
Cette première partie était dense, mais vous avez posé des bases solides. Récapitulons :
Module vs package : un module, c’est un fichier
.py; un package, c’est un dossier avec un__init__.pyqui contient plusieurs modules.Import : vous savez maintenant contrôler finement ce que vos utilisateurs peuvent importer, grâce aux imports relatifs et à la variable
__all__.pyproject.toml: vous avez construit votre premier package avec le standard moderne de packaging Python — plus besoin desetup.py!Dépendances et scripts : vous savez gérer les dépendances (obligatoires ou optionnelles) avec pixi, et déclarer des points d’entrée en ligne de commande.
Vous avez maintenant une application Python structurée et installable. Dans la partie suivante, on va s’assurer que le code est propre et bien formaté avec des outils comme ruff et pylint.