Recherche des bonnes pratiques de packaging

PyConFR 2024 - 2 novembre 2024

Françoise CONIL, Ingénieure d’études CNRS, Laboratoire LIRIS Lyon

Présentation

  • membre de l’AFPy depuis de nombreuses années
  • membre du Conseil d’Administration de l’APRIL 1 depuis février 2023
  • participe à des actions de promotion de la parité 2 3


Contexte

Laboratoire de recherche en informatique

  • Beaucoup de prototypes, pour expérimenter, obtenir des résultats et publier
  • Le code est souvent installé à partir des dépôts

1

2

  • Peu de développeurs/développeuses
  • Variété des profils : des expert·e·s aux étudiant·e·s en stage

Pourquoi packager ?

Intérêt

  • Dès que l’on a plusieurs fichiers Python 1
  • Facilite l’utilisation et la distribution du code
  • Facilite la reproductibilité du code
  • L’effort est léger quand on le fait dès le début

Mon objectif

Bases et packaging

Identifier des bonnes pratiques et informer sur :

  • des bases éventuellement sources d’erreur ou mal connues
  • des ressources
  • un processus minimal
  • des informations sur les outils et les plateformes (git, forges, Python Package Index (PyPI), frontends et backends, outils de workflow)

Difficultés

Vocabulaire troublant

Le terme package recouvre 2 usages différents 1 2

  • distribution package : un package installable 3
  • import package : généralement, un dossier qui contient des modules Python et peut être importé

ATTENTION

Les contraintes sur le nom du distribution package et le nom de l’import package diffèrent fortement et sont source de confusion.

Voir Nom de packages

Vieilles astuces

Pour permettre de faire des imports entre fichiers sans faire de package, on trouve encore des ressources qui présentent l’utilisation de sys.path.insert :

# Pour importer des modules depuis un autre répertoire, vous pouvez utiliser le code suivant :

import sys
sys.path.insert(0, '../path/to/parent/directory')

from module_name import function_name
  1. Implique d’ajouter ce type d’instruction dans tous les modules où l’on veut importer du code
  2. Devient rapidement lourd et source d’erreurs 1

Beaucoup de communautés et de besoins différents

  • Python 1.0 a 30 ans, il y a donc un long historique à gérer
  • Il y a des projets qui intègrent du code C, C++, Fortran, Rust, … qu’il faut compiler. Ce sont des projets qui étaient difficiles à installer et qui ont conduit à la création de conda

Note

Éviter de comparer Python à d’autres langages qui n’ont pas cet historique ou ces cas d’utilisation.

Lire L’installation et la distribution de paquets Python

Rappel

Je ne parle que de distribution de codes “pur Python” et de publication sur PyPI

Un écosystème devenu complexe

xkcd : Python Environment by Randall Munroe is licensed via CC BY-NC 2.5, April 30, 2018

L’auteur ne sait plus quelle est l’instance de Python que son ordinateur utilise par défaut et compare son ordinateur à un site pollué qu’il faut décontaminer.
Voir explain xkcd 1987

Un sentiment de confusion

  1. « When I started with Python and created my first package I was confused », Anna-Lena Popkes, An unbiased evaluation of environment management and packaging tools
  2. « Python packaging can be a scary and confusing endeavor », The pyOpenSci Guide to PyCon 2024
  3. « Packaging in Python has a bit of a reputation for being a bumpy ride », Overview of Python Packaging
  4. « … c’est devenu un lieu commun que cette simplicité ne s’étend pas aux outils de packaging, qui sont tout sauf faciles à maîtriser », Jean Abou Samra, L’installation et la distribution de paquets Python (1/4)
  5. « My hope is … that I can draw attention to aspects of Python packaging that are confusing … », Gregory Szorc, My User Experience Porting Off setup.py
  6. « A year or so ago, I couldn’t find a step-by-step guide to packaging a Python project », Ned Batchelder, One way to package Python code right now

Did uv solve XKCD #1987 Python Environment?

… mais uv n’est pas un backend de build

Ressources

Comprendre l’historique

2 séries d’articles intéressants en Français :

  • L’enfer des paquets Python, Guillaume Ayoub et Lucie Anglade, 7 articles, 2020
  • L’installation et la distribution de paquets Python, Jean Abou Samra, 4 articles (1/4, 2/4, …), 2023

Vidéo de la présentation “Python Packaging” de Florian Strzelecki au BreizhCamp du 30 avril 2024

Historique du packaging présenté par la Python Packaging Authority.

Python Packaging Authority

La Python Packaging Authority (PyPA) est un groupe de travail, créé en 2012 1, qui :

Scientific Python Packaging

Deux autres guides sont apparus, en 2023, pour donner des bonnes pratiques sur la façon de packager, tester et documenter les projets scientifiques en Python :

Processus minimal

Étapes

Le processus minimal est très bien illustré par Leah Wasser dans sa présentation à la PyCon US 2024 :

  1. Créer la structure du package
  2. Ajouter le code source
  3. Définir les métadonnées dans le fichier pyproject.toml
  4. Installer le package avec pip install -e .

Structure du package

Exemple de projet minimal avec une structure src layout 1 2 :

hello_pyconfr_2024            # nom du repository
├── pyproject.toml
└── src
    └── hello_pyconfr         # nom de l'import package
        ├── greetings.py
        └── __init__.py       # identifie le dossier comme un import package

Un code source basique dans greetings.py :

def greet():
    print("Hello PyConFR 2024")

Métadonnées

Fichier de métadonnées, pyproject.toml, réduit au strict minimum 1 :

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "hello_pyconfr"        # nom du distribution package
version = "0.1.0"

Installation

$ pip install -e .

editable install 1 : le flag -e permet d’installer le package localement pour continuer à le développer et à le tester sans avoir à le ré-installer.

>>> from hello_pyconfr.greetings import greet
>>> greet()
Hello PyConFR 2024

Noms de packages

Nom de distribution package et d’import package

Le nom du distribution package et du nom de l’import package n’autorisent pas les mêmes ensembles de caractères 1.

distribution package name format

A valid name consists only of ASCII letters and numbers, period, underscore and hyphen. It must start and end with a letter or number. 2

Les caractères autorisés pour le nom de l’import package sont ceux des identifiants Python. Le tiret - n’est pas autorisé !

Identifiers and keywords

Within the ASCII range (U+0001..U+007F), the valid characters for identifiers include the uppercase and lowercase letters A through Z, the underscore _ and, except for the first character, the digits 0 through 9. Python 3.0 introduced additional characters from outside the ASCII range (see PEP 3131) 3.

Exemple de noms différents

Métadonnées du pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "pytest-recording"              # nom du `distribution package`
version = "0.13.2"

Le nom du package installé (distribution package) est définit par la métadonnée name de la table [project].

$ pip install pytest-recording
Collecting pytest-recording
  Downloading pytest_recording-0.13.2-py3-none-any.whl.metadata (10 kB)
  

$ pip list
Package           Version
----------------- -------

pytest            8.3.3
pytest-recording  0.13.2


Structure du projet

pytest-recording
├──
├── docs
│   └── changelog.rst
├── pyproject.toml
├──
├── src
│   └── pytest_recording              # nom de l'`import package`
├── tests
│   ├── conftest.py
│   ├── …


Le nom de l’import package est celui du dossier sous src.

>>> from pytest_recording.plugin import RECORD_MODES
>>> RECORD_MODES
('once', 'new_episodes', 'none', 'all', 'rewrite')
>>>

Identifiants de version Python

Python package version

Les versions ne sont pas de simples chaînes de caractères.

La structure d’un identifiant de version Python 1 est spécifiée par la PEP 440 2.

Les identifiants sont normalisés afin de pouvoir être comparés à l’aide de la classe Version

>>> from packaging.version import Version

>>> Version("1.0a5")
<Version('1.0a5')>
>>> Version("v1.0")
<Version('1.0')>                     # préfixe "v" supprimé

>>> Version("1.0a5") < Version("v1.0")
True

Description des éléments de l’identifiant

  • un préfixe optionnel “v” : supprimé par la normalisation
  • un segment optionnel “epoch” : pour ordonner les projets qui passent du Calendar versioning au Semantic versioning
  • un numéro de release obligatoire de type Semantic versioning ou Calendar versioning
  • un segment optionnel “pre-release” pour désigner les versions “alpha”, “beta” et “release candidate” : normalisé aN, bN et rcN
  • un segment optionnel “post-release” : normalisé .postN
  • un segment optionnel “dev-release” : normalisé .devN
  • un segment “local”, qui a un usage spécifique, et ne peut être envoyé sur PyPI

Normalisation de l’identifiant

  • les identifiants sont transformés en minuscules
  • zéros implicites : la comparaison de segments met à 0 les segments manquants
  • les entiers sont interprétés par int(), les zéros non significatifs sont supprimés
>>> Version("1.0-alpha5")
<Version('1.0a5')>                   # Normalisation du segment "pre-release"

>>> Version("1.0.0post0")
<Version('1.0.0.post0')>             # Normalisation du segment "post-release"

>>> Version("1.0.0DEV0")
<Version('1.0.0.dev0')>              # Normalisation du segment "dev"

>>> v_1_0 = Version("1.0")
<Version('1.0')>
>>> v_1_0.epoch, v_1_0.micro         # zéros implicites
(0, 0)
>>> v_1_0 == Version("0!1.0.0")
True

>>> Version("01.001.0000")           # segments entiers interprétés par int()
<Version('1.1.0')>
>>> Version("2023.04")
<Version('2023.4')>

>>> Version("1!1.0.0") > Version("2023.04")  # "epoch" utilisé pour imposer l'ordre des versions
True

git tag et git describe

Deux types de tags

Pour préparer la diffusion d’une version d’un package, on commence par étiqueter (tag) un point de l’historique avec un identifiant de version.

La commande git tag 1 permet de créer 2 types de tags :

  • annotated tag : stocké en tant qu’objet git

  • lightweight tag : juste un pointeur sur un commit particulier

Annotated tag

Il est recommandé de créer un annotated tag pour un identifiant de version.

Un annotated tag est un objet git avec plusieurs propriétés : somme de contrôle, nom de l’annotateur, mail, date du tag et éventuellement signature.

$ git tag -a 0.1.0 -m "Nice greeting script"    # L'option -a crée un annotated tag

$ git show -s 0.1.0
tag 0.1.0
Tagger: Françoise Conil <fcodvpt@gmail.com>
Date:   Wed Oct 18 15:51:06 2023 +0200

Nice greeting script

commit 419bfb10b5051259ac9702ac5af244461c11385e (HEAD -> main, tag: 0.1.0)
Author: Françoise Conil <fcodvpt@gmail.com>
Date:   Wed Oct 18 15:50:44 2023 +0200

    Polite greetings

ATTENTION

Il faut explicitement envoyer les annotated tag lors du push vers le remote avec l’option --follow-tags :

$ git push origin --follow-tags

en supposant que remote=origin

Lightweight tag

Un lightweight tag est juste un pointeur sur un commit particulier 1.

$ git tag 0.1.1       # Crée un lightweight tag car les options `-a`, `-m`, `-s` sont absentes

$ git show -s 0.1.1
commit 6a269e0f488597524b1a86db89f8a688316f8b11 (HEAD -> main, tag: 0.1.1)
Author: Françoise Conil <fcodvpt@gmail.com>
Date:   Wed Oct 18 15:52:21 2023 +0200

    add a date

Il n’y a alors pas d’objet tag git créé avec les métadonnées de tag.

$ git cat-file -t 0.1.0     # annotated tag
tag

$ git cat-file -t 0.1.1     # lightweight tag
commit

git describe

La commande git describe détecte l’annotated tag le plus récent accessible depuis le commit courant.

$ git log --oneline -n 20
5fe6b80 (HEAD -> master, origin/master, origin/HEAD) docs(changelog): Add entry for previous commit
4f37c30 build: Update classifiers
8e65de4 ci: Test on Python 3.12
002d6a7 docs(csvgrep): Simplify xargs command with $0. Use 22 not 222.
c4e5612 docs(csvgrep): Use variable ($1) instead of replstr to avoid quoting issues
4e0b890 docs(csvgrep): Quote the replstr using single quotes
9d06655 docs(csvgrep): Quote the replstr
66e7086 docs: Document how to get the indices of the columns that contain matching text, #1209
1202688 (tag: 1.2.0) build: Iterate the version number
82233b8 test: Try engine.dispose() to release file handle for SQLite database in Windows tests
034a92c ci: Attempt to expose OSError on Windows

La commande git describe affiche :

  • le tag trouvé : 1.2.0
  • le nombre de commits depuis ce tag : 8
  • l’identifiant du commit courant : 5fe6b80
$ git describe
1.2.0-8-g5fe6b80

forges (GitHub, GitLab)

Avec les tags sur GitHub

Si des tags 1 ont été positionnés dans l’historique du code ET envoyés sur GitHub, on peut consulter la liste des tags et récupérer un zip ou un tar.gz du projet, pour chaque tag, sans action supplémentaire.

ATTENTION

Il semble que les tags créés par GitHub soient des lightweight tags et non des annotated tags.

Cela peut perturber certains outils utilisés pour le packaging comme dans l’issue 521 de setuptools-scm.

Avec les tags sur GitLab

Si des tags ont été positionnés dans l’historique du code ET envoyés sur GitLab, on peut consulter la liste des tags et récupérer un zip, un tar.gz, un tar.bz2 ou un tar du projet, pour chaque tag, sans action supplémentaire.

PyPI

PyPI : Présentation

PyPI est le standard pour le dépôt de packages Python.

  • 35,7 milliards de téléchargements en 2022, augmentation annuelle de 57% du nombre de téléchargements et de la bande passante 1
  • Près de 450 000 projets hébergés sur PyPI 1
  • PyPI n’avait que 3 mainteneurs / administrateurs en 2016 2
  • Essentiel des soutiens via la mise à disposition de services à titre gratuit par quelques entreprises 2, 3

LIMITATIONS

Limitations sur la taille des fichiers uploadés (100 MiB) et sur la taille totale des projets sur PyPI (10 GiB) 4

PyPI : Documentation et tests

Documentations :

TestPyPI

Testez l’upload de vos packages sur l’infrastructure de test : TestPyPIhttps://test.pypi.org/

Suppression

La suppression de projet ou de version est irréversible.

Il n’est pas possible de supprimer et re-publier une version, il faut incrémenter le numéro de version.

PyPI : Authentification à 2 facteurs

PyPI a mis en place l’authentification à 2 facteurs (2FA) 1 fin 2023 2.

La double authentification PyPI impose l’utilisation d’un périphérique de sécurité 3 ou d’une application d’authentification 4

Processus de connexion

L’authentification à deux facteurs affecte uniquement la connexion via un navigateur Web, et pas (encore) la publication des paquets.

PyPI : Jeton d’API

Il est recommandé d’utiliser un jeton d’API 1 pour publier sur PyPI. On peut créer :

  • un jeton global pour tous les projets de son compte PyPI (Paramètres du compte)
  • ou des jetons dont la portée est limitée à un projet (Paramètres du projet)

Pour utiliser ces jetons d’API :

  • nom d’utilisateur·rice = __token__
  • mot de passe = valeur du jeton (inclure le préfixe pypi-)

Le stockage de ces paramètres dépend de l’outil utilisé : fichier .pypirc, variables d’environnement, application de gestion de mots de passes du système 2, …

PyPI : Personnes en charge du projet

Pour définir les personnes en charge du projet, il faut se connecter à PyPI et aller dans la gestion du projet concerné.

Cliquer alors sur le menu Personnes et définir les gestionnaires du projet en saisissant l’identifiant de leur compte PyPI et leur rôle : owner (tous les droits) ou maintainer.

Note

À ce jour, il ne semble pas y avoir de lien entre les métadonnées authors et maintainers du package et les comptes gestionnaires du projet sur PyPI.

De setup.py à aujourd’hui

Historiquement

Le packaging a longtemps a reposé sur setuptools et sur le script setup.py. Cela posait plusieurs problèmes dont la nécessité d’exécuter ce script pour déterminer comment installer un package, connaître ses dépendances, etc 1

Il fallait également décorréler l’outil d’installation pip de l’exécution de setuptools avec setup.py.

L’utilisation d’un format déclaratif pour les métadonnées de package a été d’abord implémentée via une solution intégrée à setuptools : setup.cfg 2.

Les PEP 517 et PEP 518 ont défini les notions de build frontend (pip, build, uv), de build backend (hatchling, setuptools, …) et un nouveau fichier pour les métadonnées : pyproject.toml

Ce découplage frontend / backend a permis de faire évoluer pip et setuptools et de faire apparaître de nouveaux outils.

pyproject.toml

Le fichier pyproject.toml est un fichier texte, au format TOML 1 2.

Il sert de fichier de configuration pour les outils de packaging mais aussi pour les linters, les “type checkers”, les outils de test, …

Il peut comporter des champs dynamiques (version, description, …) dont le fonctionnement dépend du backend de build.


Consulter

Définitions

La PEP 517 définit les rôles du frontend et du backend de build 1

A build frontend is a tool that users might run that takes arbitrary source trees or source distributions and builds wheels from them.

The actual building is done by each source tree’s build backend.

Build frontend

Le fichier pyproject.toml donne au frontend de build (pip, build, uv) les informations nécessaires pour créer un environnement de génération du package et invoquer le build backend.

Via la table [build-system], le build frontend sait :

  • qu’il a besoin d’un “environnement” Python avec cffi, packaging, …
  • qu’il doit appeler le build backend setuptool pour générer le package pyzmq
  • qu’il a besoin d’une version minimale supérieure à 61 pour setuptool
[build-system]
requires = [
  "cffi; implementation_name == 'pypy'",
  "cython>=3.0.0; implementation_name == 'cpython'",
  "packaging",
  "setuptools>=61",
  "setuptools_scm[toml]",
]
build-backend = "setuptools.build_meta"

[project]
name = "pyzmq"

Build backend

D’autres build backend que setuptools sont apparus pour construire des packages. 1

Via la table [build-system], le build frontend sait :

  • qu’il a besoin d’un “environnement” Python avec hatch-vcs et hatch-fancy-pypi-readme (en version supérieure à 22.8.0)
  • qu’il doit appeler le build backend hatchling pour générer le package structlog
[build-system]
requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme>=22.8.0"]
build-backend = "hatchling.build"

[project]
dynamic = ["readme", "version"]
name = "structlog"

Génération du package par le frontend build

Le frontend build crée un environnement “isolé” où il installe les requires spécifiés dans build-system pour générer les packages source sdist et binaire wheel du projet structlog.

$ git clone https://github.com/hynek/structlog.git
$ cd structlog
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install --upgrade pip build
$ python -m build
* Creating venv isolated environment...
* Installing packages in isolated environment... (hatch-fancy-pypi-readme>=22.8.0, hatch-vcs, hatchling)
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating venv isolated environment...
* Installing packages in isolated environment... (hatch-fancy-pypi-readme>=22.8.0, hatch-vcs, hatchling)
* Getting build dependencies for wheel...
* Building wheel...
/tmp/build-env-br9rr4sl/lib/python3.10/site-packages/setuptools_scm/git.py:308: UserWarning: git archive did not support describe output
  warnings.warn("git archive did not support describe output")
/tmp/build-env-br9rr4sl/lib/python3.10/site-packages/setuptools_scm/git.py:327: UserWarning: unprocessed git archival found (no export subst applied)
  warnings.warn("unprocessed git archival found (no export subst applied)")
Successfully built structlog-23.2.1.dev34.tar.gz and structlog-23.2.1.dev34-py3-none-any.whl
$ ls dist/
structlog-23.2.1.dev34-py3-none-any.whl  structlog-23.2.1.dev34.tar.gz

Génération du package par le frontend pip

$ git clone https://github.com/hynek/structlog.git
$ cd structlog
$ git describe
23.2.0-34-g8d3eeb1
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install --upgrade pip
$ python -m pip wheel --wheel-dir=dist .
Processing /home/fconil/LogicielsSrc/structlog
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: structlog
  Building wheel for structlog (pyproject.toml) ... done
  Created wheel for structlog: filename=structlog-23.2.1.dev34-py3-none-any.whl size=63292 sha256=f0afd834471686ece125bc56332b47b77eeda85377ca79cc378dd36f19159d4f
  Stored in directory: /tmp/pip-ephem-wheel-cache-iru73n78/wheels/d4/a0/d1/88d4397a5f4751562af152ee044e264ac0fb9f7d6be1c3002d
Successfully built structlog
$ ls dist
structlog-23.2.1.dev34-py3-none-any.whl

Installation

$ python -m pip install dist/structlog-23.2.1.dev34-py3-none-any.whl
Processing ./dist/structlog-23.2.1.dev34-py3-none-any.whl
Installing collected packages: structlog
Successfully installed structlog-23.2.1.dev34
$ pip list
Package    Version
---------- ------------
pip        23.3.1
setuptools 59.6.0
structlog  23.2.1.dev34

Explosion des outils

xkcd : Standards by Randall Munroe is licensed via CC BY-NC 2.5, July 20, 2011

Comment choisir : QSOS

La méthode de Qualification et de Sélection de logiciels Open Source (QSOS) est une méthode d’analyse, de comparaison et de choix de logiciels libres 1.

La méthode définit des critères 2 pour effectuer une sélection :

Problème : long et pas toujours facile de trouver les informations

Comment choisir : fonctionnalités des outils

An unbiased evaluation of environment management and packaging tools” by Anna-Lena Popkes is licensed via CC BY-NC-ND 4.0, August 24, 2023

Statistiques sur le code 1/2

Statistiques de commits au 18/10/2024 (gitstats)

Outil Premier commit Dernier commit Nb commits Nb commit per active day Nb authors
Flit 2015-03-13 2024-05-29 1198 3.0 79
Hatch 2021-12-29 2024-10-14 965 2.9 77
PDM 2019-12-27 2024-10-17 2896 3.3 203
setuptools 1998-12-18 2024-10-17 15689 4.7 635
Poetry 2018-02-20 2024-02-25 3110 3.0 548
uv 2023-10-04 2024-10-18 4419 12.3 208

Principaux contributeurs

Flit Hatch PDM setuptools Poetry uv
1 890 (74.29%) 1 825 (85.49%) 1 2213 (76.42%) 1 6745 (42.99%) 1 1049 (34.68%) 1 2000 (45.26%)
2 46 (3.84%) 2 (bot) 21 (2.18%) idem 175 (6.04%) 2 1851 (11.80%) 2 353 (11.35%) 2 838 (18.96%)
3 26 (2.17%) 3 12 (1.24%) 3 53 (1.83%) 3 632 (4.03%) 3 177 (5.69%) 3 539 (12.20%)

Date de leur dernier commit

Flit Hatch PDM setuptools Poetry uv
1 2024-05-29 1 2024-10-14 1 2024-10-17 1 2024-09-16 1 2022-09-18 1 2024-10-17
2 2023-11-10 2 (bot) 2024-07-01 idem 2020-09-04 2 2024-10-17 2 2024-02-25 2 2024-10-18
3 2021-03-01 3 2023-04-02 3 2021-05-05 3 2001-08-23 3 2024-02-25 3 2024-10-17

Statistiques sur le code 2/2

Flit

Hatch

PDM

Setuptools

Poetry

uv

Statistiques PyPI 1/2

Statistiques PyPI - log scale 2/2

build backends

Liste de backends

9 outils classés “Package building” dans le diagramme de Venn des outils de packaging :

Outil Backend Remarque
setuptools setuptools Support C, C++ et Go, Rust via des plugins
Poetry poetry-core
Flit flit_core
Hatch hatchling
PDM pdm-backend
Maturin maturin Support Rust via Cargo
enscons enscons Support C … SCons
Rye Replaced by uv, pas un backend ?
Pyflow dernière release 2021, pas un backend ?

Autres backends qui supportent l’intégration de différents langages 1

Outil Backend Remarque
meson-python mesonpy Support C, C++, Fortran, Rust, … Meson
scikit-build-core scikit-build-core Support C, C++, Fortran, … CMake

backend : setuptools

setuptools est l’outil historique de packaging Python (1998 ?).

  • premier backend présent dans les projets PyPI 1, utilisable avec pyproject.toml 2
  • toujours activement développé, plusieurs devs 3
  • peut extraire dynamiquement la version du package de git 4
  • peut gérer des packages qui contiennent du code C/C++ 5

Déclaration du backend dans le pyproject.toml :

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

backend : poetry-core

Poetry est un outil de packaging et de gestion de dépendances apprécié par la communauté.

  • créé en 2018, c’est le deuxième backend présent dans les projets PyPI
  • peut extraire dynamiquement la version du package de git
  • projet externe à la Python Packaging Authority
  • développement en déclin ? Plusieurs devs 1

Déclaration du backend dans le pyproject.toml :

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Warning

Je n’ai pas réussi à l’utiliser comme backend avec python -m build

backend : flit_core

Flit est un outil de packaging Python simple à prendre en main et à utiliser.

  • créé en 2015, un des projets PyPA, auteur impliqué dans plusieurs PEP 517, 600, 621, 571
  • sert à packager des projets pur Python
  • ne permet pas d’extraire dynamiquement la version du package de git
  • développement en déclin ? Un dev très majoritairement 1

Déclaration du backend dans le pyproject.toml :

[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

backend : hatchling

Hatch est l’outil mis en avant par les membres de la PyPA et dans les guides cités 1

  • créé en 2019, un des projets PyPA, auteur impliqué dans plusieurs PEP 752, 755, 723, 631
  • backend par défaut lors de la création de projet par uv init 2
  • peut extraire dynamiquement la version du package de git
  • un dev très majoritairement 3

Déclaration du backend dans le pyproject.toml :

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

backend : pdm-backend

PDM est mis en avant dans le guide PyOpenSci 1

  • créé en 2019, projet externe à la Python Packaging Authority
  • la documentation est axée sur la partie frontend de l’outil
  • un dev très majoritairement 2

Déclaration du backend dans le pyproject.toml :

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

Outils de workflow

Workflow tools

La notion de “Workflow tools” 1 paraît mieux adaptée pour qualifier :

  • Poetry, le premier outil complet de ce type d’où son succès
  • Hatch, lié au backend hatchling, son utilisation s’est développée
  • PDM, lié mais pas restreint au backend pdm-backend
  • uv, le plus récent, probable futur outil de référence
  • Pipenv, gère les environnements et les dépendances mais pas le packaging
  • Flit, lié au backend flit_core, reste simple

En effet, ces outils permettent de réaliser tout ou partie des tâches suivantes :

  • de générer un squelette de projet
  • d’uploader les packages vers PyPI
  • de gérer les environnements virtuels
  • d’installer des versions de Python
  • d’installer des packages
  • de gérer les dépendances des packages installés
  • de lancer des matrices de tests
  • d’exécuter des commandes

Mes outils

J’utilise un ensemble d’outils qui font chacun une tâche (ou à peu près), qui sont simples à utiliser et plutôt transparents :

  • la librairie intégrée (builtin) venv pour les environnements virtuels
  • pip pour l’installation de packages 1
  • build comme frontend de build 1
  • Flit comme backend
  • pip-tools pour la gestion des dépendances

Mais ce fonctionnement (plusieurs outils différents) ne convient pas à tout le monde et je peux avoir à intervenir sur un projet qui aura des besoins plus complexes.

uv

Sorti en février 2024, uv est annoncé 1 comme un remplaçant de pip et de pip-tools.

La version 0.3.0 annoncée 2 en août déclenche beaucoup d’enthousiasme 3 mais suscite aussi des inquiétudes 4.

  • remplace pip, pip-tools, pipx, poetry (?), pyenv, virtualenv, et plus (?)
  • 10 à 100 fois plus rapide que pip
  • installe et gère plusieurs versions de Python
  • installe et exécute des applications (pipx)
  • gère les dépendances et génère des fichiers de lock cross-plateform (spécification ?)
  • fonctionne sur macOS, Linux, et Windows

Un mot sur conda

Présentation

conda est l’outil de gestion de packages pour les installations de Python Anaconda. 1

Il est très utilisé par la communauté scientifique, en particulier sous Windows où l’installation d’extensions binaires était souvent difficile.

conda est un outil complètement séparé des outils pip, virtualenv et wheel.

conda n’installe pas de package depuis PyPI et ne peut installer des packages que depuis le dépôt officiel Anaconda, ou anaconda.org ou un serveur de packages local.

pip doit être installé et fonctionner en parallèle de conda pour installer des packages depuis PyPI.

conda skeleton

conda skeleton permet de créer des packages conda à partir de package téléchargés depuis PyPI en modifiant leurs métadonnées.

Changement de licence

Je travaille pour un institut de recherche en Europe. Nous avons dû bloquer dans l’urgence la plupart des domaines anaconda.org / .cloud / .com en raison de menaces juridiques d’Anaconda. 1

Sur la base de leurs tarifs, si vous avez plus de 200 utilisateurs employés, vous devez désormais payer 50 $ par utilisateur (10 000 $) par mois.

Il ne semble pas que tous nos instituts de recherche publics soient considérés comme académiques.

Les utilisateurs qui ne sont pas considérés comme enseignement ou académiques par Anaconda 2 sont en train de migrer vers d’autres canaux de distribution : conda-forge 3, bioconda, …

Conclusion

Du processus simple aux outils de workflow

Il est possible de démarrer simplement avec un processus minimal :

  1. Créer la structure du package (src layout)
  2. Ajouter le code source
  3. Définir les métadonnées dans le fichier pyproject.toml
  4. Installer le package avec pip install -e .

Ensuite on peut faire évoluer ses outils en fonction des besoins.

Je vais tester uv comme outil de workflow et hatchling comme backend.

Annexes

Illustration avec des noms différents

Métadonnées du pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "Librairie-francoise"           # nom du `distribution package`
version = "0.1.0"

[tool.hatch.build.targets.wheel]
packages = ["src/LIBRAIRIE_françoise"] # nom de l'`import package`

Le nom du package installé (distribution package) est définit par la métadonnée name de la table [project].

$ pip install librairie_francoise-0.1.0-py2.py3-none-any.whl

$ pip list
Package             Version
------------------- -------
Librairie-francoise 0.1.0
pip                 24.3.1
setuptools          59.6.0


Structure du projet

hello-pyconfr_2024
├── pyproject.toml
└── src
    └── LIBRAIRIE_françoise            # nom de l'`import package`
        ├── greetings.py
        └── __init__.py


Le nom de l’import package est celui du dossier sous src.

>>> from LIBRAIRIE_françoise.greetings import greet
>>> greet()
Hello PyConFR 2024

Il est parfois nécessaire de le spécifier explicitement dans le pyproject.toml, suivant le backend et la structure du dossier.

SemVer : Semantic Versioning

Le Semantic Versioning identifie les versions à l’aide de 3 nombres : MAJOR.MINOR.PATCH

Ces nombres doivent être incrémentés :

  1. MAJOR : quand il y a des changements non rétrocompatibles,
  2. MINOR : quand il y a des ajouts de fonctionnalités rétrocompatibles,
  3. PATCH : quand il y a des corrections de bugs rétrocompatibles.

On peut ajouter un identifiant de pre-release et un identifiant de build.

CalVer : Calendar Versioning

Un identifiant en Calendar Versioning peut comporter jusqu’à 4 segments : MAJOR.MINOR.MICRO.MODIFIER

MAJOR, MINOR et MICRO sont des nombres qui peuvent correspondre à des portions de dates.

Segment Description Exemples
YYYY Full year 2006, 2016, 2106
YY Short year 6, 16, 106
0Y Zero-padded year 06, 16, 106
MM Short month 1, 2 … 11, 12
0M Zero-padded month 01, 02 … 11, 12
WW Short week (since start of year) 1, 2, 33, 52
0W Zero-padded week 01, 02, 33, 52
DD Short day 1, 2 … 30, 31
0D Zero-padded day 01, 02 … 30, 31

Les logiciels sont libres de formatter les identifiants à leur convenance.

CalVer : exemples

Applications
Project CalVer Format Examples
Ubuntu YY.0M 4.10 - 20.04
JetBrains PyCharm YYYY.MINOR.MICRO 2017.1.2
ArchLinux YYYY.0M.0D 2018.03.01
Standards
Project CalVer Format Examples
C++ YY 98, 03, 11, 14, 17
ECMAScript (aka JavaScript ) YYYY 2015, 2020
Libraries and Utilities
Project CalVer Format Examples
pytz YYYY.MM 2016.4
pip YY.MINOR.MICRO 19.2.3
youtube_dl YYYY.0M.0D.MICRO 2016.06.19.1

PyPI : Activation authentification 2FA

PyPI : fichier .pypirc

PyPA specification : The .pypirc file

On peut configurer l’utilisation de l’authentification dans le fichier .pypirc utilisé par plusieurs outils de publication comme twine ou backend comme Flit

[distutils]
index-servers =
   pypi
   testpypi
   hello_pyconfr

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__

[hello_pyconfr]
repository = https://test.pypi.org/legacy/
username = __token__

PyPI : Trusted Publisher

La notion de Trusted Publisher repose sur OpenID Connect pour permettre de connecter un “environnement automatisé” de type CI 1 à PyPI.

La documentation et l’interface semblent essentiellement orientées GitHub Action 2 3

Warning

Je n’ai pas testé cette fonctionnalité. Lire attentivement :

car pousser automatiquement des versions pose un certain nombre de questions. 4

PyPI : Organizations

Les organisations PyPI sont une notion récente 1, elles ont pour but :

  • de faciliter l’identification de la provenance d’un package 2
  • de permettre aux grosses communautés et sociétés de gérer de multiples équipes, membres, projet avec différentes permissions

Une organisation est payante pour une société et gratuite pour une communauté.

Une organisation ne permet pas de détenir des packages “privés” 3.

Critères de Maturité QSOS : Patrimoine

Patrimoine : Historique et patrimoine du projet

+ Age du projet :
    - 0 :  Inférieur à trois mois
    - 1 :  Entre trois mois et trois ans
    - 2 :  Supérieur à trois ans

+ Historique :
    - 0 :  Le logiciel connaît de nombreux problèmes qui peuvent être rédhibitoires
    - 1 :  Pas de problèmes majeurs, ni de crise ou historique inconnu
    - 2 :  Bon historique de gestion de projet et de crise

+ Equipe de développement :
    - 0 :  Très peu de développeurs identifiés ou développeur unique
    - 1 :  Quelques développeurs actifs
    - 2 :  Equipe de développement importante et identifiée

+ Popularité :
    - 0 :  Très peu d'utilisateurs identifiés
    - 1 :  Usage décelable
    - 2 :  Nombreux utilisateurs et références

Critères de Maturité QSOS : Activité

Activité : Activité du et autour du projet

+ Communauté des contributeurs :
    - 0 :  Pas de communauté ou de réelle activité (forum, liste de diffusion…)
    - 1 :  Communauté existante avec une activité notable
    - 2 :  Communauté forte : grosse activité sur les forums, de nombreux contributeurs et défenseurs

+ Activité autour des bugs :
    - 0 :  Réactivité faible sur le forum ou sur la liste de diffusion, ou rien au sujet des corrections 
           de bugs dans les notes de versions
    - 1 :  Activité détectable mais sans processus clairement exposé, temps de résolution long
    - 2 :  Forte réactivité, basée sur des rôles et des assignations de tâches

+ Activité autour des fonctionnalités :
    - 0 :  Pas ou peu de nouvelles fonctionnalités
    - 1 :  Évolution du produit conduite par une équipe dédiée ou par des utilisateurs, mais sans processus 
           clairement exposé
    - 2 :  Les requêtes pour les nouvelles fonctionnalités sont clairement outillées, feuille de route disponible

+ Activité sur les releases/versions :
    - 0 :  Très faible activité que ce soit sur les versions de production ou de développement (alpha, beta)
    - 1 :  Activité que ce soit sur les versions de production ou de développement (alpha, beta),
           avec des versions correctives mineures fréquentes
    - 2 :  Activité importante avec des versions correctives fréquentes et des versions majeures planifiées 
           liées aux prévisions de la feuille de route

Critères de Maturité QSOS : Gouvernance

Gouvernance : Stratégie du projet

+ Détenteur des droits :
    - 0 :  Les droits sont détenus par quelques individus ou entités commerciales
    - 1 :  Les droits sont détenus par de nombreux individus de façon homogène
    - 2 :  Les droits sont détenus par une entité légale, une fondation dans laquelle la communauté a confiance
           (ex: FSF, Apache, ObjectWeb)

+ Feuille de route :
    - 0 :  Pas de feuille de route publiée
    - 1 :  Feuille de route sans planning
    - 2 :  Feuille de route versionnée, avec planning et mesures de retard

+ Pilotage du projet :
    - 0 :  Pas de pilotage clair du projet
    - 1 :  Pilotage dicté par un seul individu ou une entité commerciale
    - 2 :  Indépendance forte de l'équipe de développement, droits détenus par une entité reconnue

+ Mode de distribution :
    - 0 :  Existence d'une distribution commerciale ou propriétaire ou distribution libre limitée fonctionnellement
    - 1 :  Sous-partie du logiciel disponible sous licence propriétaire (Coeur / Greffons...)
    - 2 :  Distribution totalement ouverte et libre

Critères de Maturité QSOS : Industrialisation

Industrialisation : Niveau d’industrialisation du projet

+ Services : Offres de services (Support, Formation, Audit...)
    - 0 :  Pas d'offre de service identifiée
    - 1 :  Offre existante mais restreinte géographiquement ou en une seule langue ou fournie par un seul 
           fournisseur ou sans garantie
    - 2 :  Offre riche, plusieurs fournisseurs, avec des garanties de résultats

+ Documentation :
    - 0 :  Pas de documentation utilisateur
    - 1 :  La documentation existe mais est en partie obsolète ou restreinte à une seule langue ou peu détaillée
    - 2 :  Documentation à jour, traduite et éventuellement adaptée à différentes cibles de lecteurs
           (enduser, sysadmin, manager...)

+ Méthode qualité : Processus et méthode qualité
    - 0 :  Pas de processus qualité identifié
    - 1 :  Processus qualité existant, mais non formalisé ou non outillé
    - 2 :  Processus qualité basé sur l'utilisation d'outils et de méthodologies standards

+ Modification du code :
    - 0 :  Pas de moyen pratique de proposer des modifications de code
    - 1 :  Des outils sont fournis pour accéder et modifier le code (ex : CVS, SVN) mais ne sont pas vraiment
           utilisés pour développer le produit
    - 2 :  Le processus de modification de code est bien défini, exposé et respecté, basé sur des rôles bien définis

Template de projet Cookiecutter

Le « Scientific Python Library Development Guide » propose des templates de projets pour Cookiecutter ci-dessous, ainsi que pour Copier et cruft

$ cookiecutter gh:scientific-python/cookie
You've downloaded /home/fconil/.cookiecutters/cookie before. Is it okay to delete and re-download it? [y/n] (y): y
  [1/9] The name of your project (package): hello_pyconfr
  [2/9] The name of your (GitHub?) org (org): fconil
  [3/9] The url to your GitHub or GitLab repository (https://github.com/fconil/hello_pyconfr):
  [4/9] Your name (My Name): Françoise CONIL
  [5/9] Your email (me@email.com): fcodvpt@gmail.com
  [6/9] A short description of your project (A great package.): Test cookiecutter and scientific-python templates for Python packaging
  [7/9] Select a license
    1 - BSD
    2 - Apache
    3 - MIT
    Choose from [1/2/3] (1): 1
  [8/9] Choose a build backend
    1 - Hatchling                      - Pure Python (recommended)
    2 - Flit-core                      - Pure Python (minimal)
    3 - PDM-backend                    - Pure Python
    4 - Poetry                         - Pure Python
    5 - Setuptools with pyproject.toml - Pure Python
    6 - Setuptools with setup.py       - Pure Python
    7 - Setuptools and pybind11        - Compiled C++
    8 - Scikit-build-core              - Compiled C++ (recommended)
    9 - Meson-python                  - Compiled C++ (also good)
    10 - Maturin                       - Compiled Rust (recommended)
    Choose from [1/2/3/4/5/6/7/8/9/10] (1): 1
  [9/9] Use version control for versioning [y/n] (y): y

Voici l’Arborescence du projet généré :

$ tree -a hello_pyconfr/
hello_pyconfr/
├── docs
│   ├── conf.py
│   └── index.md
├── .git_archival.txt
├── .gitattributes
├── .github
│   ├── CONTRIBUTING.md
│   ├── dependabot.yml
│   ├── release.yml
│   └── workflows
│       ├── cd.yml
│       └── ci.yml
├── .gitignore
├── LICENSE
├── noxfile.py
├── .pre-commit-config.yaml
├── pyproject.toml
├── README.md
├── .readthedocs.yaml
├── src
│   └── hello_pyconfr
│       ├── __init__.py
│       ├── py.typed
│       └── _version.pyi
└── tests
    └── test_package.py