Skip to article frontmatterSkip to article content

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 .py que 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 - b

Utilisation d’un module

Il existe différentes manières d’importer un module en utilisant le mot-clef import.

  1. On peut importer un module via son nom.
import calculator_mod
calculator_mod.add(1, 2)
  1. 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)
  1. On peut importer l’ensemble du module.
from calculator_mod import *
add(1, 2)

import 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__)

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))
! python calculator_mod.py

Un package

Comme dit en introduction, un package est un ensemble de modules Python. Prenons l’arborescence suivante

! tree examples/simple_calculator/

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 add

On a alors le comportement suivant

import calculator
calculator.add(1, 2)
calculator.sub.sub(2,3)
Solution to Exercise 1
  1. non
  2. oui
  3. non
  4. 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.toml
  • README.rst ou README.md
  • LICENSE.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.toml

Premier 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-system indique les outils pour construire le package ainsi que le backend utilisé. Nous utiliserons tout au long de cet atelier setuptools mais il en existe bien d’autres[1] (Hatch, PDM, Flit, Whey, ...).
  • project dé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

  1. pip install . pour installer le package dans le répertoire site-packages où se trouve votre version de Python.

  2. pip install . --user pour l’installer dans votre $HOME.

  3. 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 numpy

et 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 nbsphinx

ce 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 doc

Ce 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 pytest

Nous 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"

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 utilise 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"

Exercices

Etape 1

Dans le répertoire step0, vous avez l’arborescence suivante

! tree TPs/1.packaging/step0/

Etape 2

Etape 3

Footnotes