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)3- On peut importer une partie d’un module.
from calculator_mod import sub
sub(1, 2)-1- On peut importer un module en modifiant son nom d’appel.
import calculator_mod as calc
calc.add(1, 2)3- On 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
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
│ ├── __pycache__
│ │ ├── __init__.cpython-311.pyc
│ │ └── __init__.cpython-312.pyc
│ └── operator
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-311.pyc
│ │ ├── __init__.cpython-312.pyc
│ │ ├── add.cpython-311.pyc
│ │ ├── add.cpython-312.pyc
│ │ ├── sub.cpython-311.pyc
│ │ └── sub.cpython-312.pyc
│ ├── add.py
│ └── sub.py
└── pyproject.toml
5 directories, 13 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
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
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.txt- votre
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. pixiest 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/