---
title: "Versionner un code Python"
author: "Françoise CONIL"
format:
  revealjs:
    theme: [solarized, custom.scss]
    slide-number: true
    show-slide-number: all
    code-line-numbers: false
    link-external-newwindow: true
---

## Présentation

Quelles sont les différentes étapes et quels sont les choix possibles pour
versionner son code Python ?

- Structure d'un identifiant de version Python
- Logiciel de gestion de version : git
- Forges (GitHub, GitLab)
- Librairies de gestion de l'identifiant de version (setuptools_scm,
  Versioneer, Miniver, ...)

::: notes
Cette présentation ne décrit pas toutes les étapes nécessaires au packaging
d'une application.
:::

# Structure d'un identifiant de version Python

## Python package version

La structure d'un **identifiant de version Python**, spécifiée par la PEP 440
^[[PEP 440 - Version Identification and Dependency Specification](https://peps.python.org/pep-0440/)], 
est la suivante ^[Informations et illustrations extraites de l'article [What is a Python package version?](https://sethmlarson.dev/pep-440#v-prefixes) de [Seth Michael
Larson](https://sethmlarson.dev/) sous licence [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/?ref=chooser-v1)] :

![](images/python-version-identifiers.png){height="120"}

Les versions ne sont pas de simples chaînes de caractères, elles sont
[normalisées](https://peps.python.org/pep-0440/#normalization) afin de pouvoir
être comparées à l'aide de la [classe Version](https://packaging.pypa.io/en/latest/version.html)

```{.python}
>>> from packaging.version import Version, parse
>>> v1 = parse("1.0a5")
>>> v2 = Version("v1.0")
>>> v1
<Version('1.0a5')>
>>> v2
<Version('1.0')>
>>> v1 < v2
True
```

## Description des éléments de l'identifiant

- un préfixe optionnel "v", supprimé par la normalisation (`v0.1`=> `0.1`)
- un segment optionnel "epoch", utilisé pour ordonner correctement les projets
  qui passent du [Calendar versioning](https://calver.org/) au [Semantic
  versioning](https://semver.org/) <br/>(`1!1.0.0` > `2023.10`)
- un **numéro de release obligatoire** de type [Semantic versioning](https://semver.org/)
  (`2.12.1`) ou [Calendar versioning](https://calver.org/) (`2023.04`)
- 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
  [postPyPI](https://pypi.org/)

::: aside
Informations issues de l'article [What is a Python package version?](https://sethmlarson.dev/pep-440#v-prefixes)
sur la [PEP 440](https://peps.python.org/pep-0440/)
:::

## Normalisation de l'identifiant 1/2

- les identifiants sont toujours transformés en minuscules
- il y a pas de limite de taille à un identifiant

```{.python}
>>> from packaging.version import Version, parse
>>> v_v = Version("v1.0.1")          # "v" prefix removed
>>> v_v
<Version('1.0.1')>
>>> v_cal = Version("2023.04")
>>> v_epoch = Version("1!1.0.0")     # "epoch" can be used to act on version ordering
>>> v_epoch > v_cal
True
>>> v_alpha = Version("1.0-alpha5")  # "pre-release" normalization
>>> v_alpha
<Version('1.0a5')>
>>> v_post = Version("1.0.0post0")   # "post-release" normalization
>>> v_post
<Version('1.0.0.post0')>
>>> v_dev = Version("1.0.0dev0")     # "dev" normalization
>>> v_dev
<Version('1.0.0.dev0')>
```

::: aside
Informations issues de l'article [What is a Python package version?](https://sethmlarson.dev/pep-440#v-prefixes)
sur la [PEP 440](https://peps.python.org/pep-0440/)
:::

::: {.notes}
code-line-numbers="2,4"
:::

## Normalisation de l'identifiant 2/2

- zéros implicites : la comparaison de segments met à 0 les segments manquants
- les entiers étant interprétés par `int()`, les zéros non significatifs sont
  supprimés

```{.python}
>>> v_1_0 = Version("1.0")           # zéros implicites
>>> v_1_0
<Version('1.0')>
>>> v_1_0.micro
0
>>> v_1_0.epoch
0
>>> v_1_0 == Version("0!1.0.0")
True
>>> Version("01.001.0000")           # zéros non significatifs
<Version('1.1.0')>
>>> Version("2023.04")
<Version('2023.4')>
```
::: aside
Informations issues de l'article [What is a Python package version?](https://sethmlarson.dev/pep-440#v-prefixes)
sur la [PEP 440](https://peps.python.org/pep-0440/)
:::

## SemVer : Semantic Versioning

Le [Semantic Versioning](https://semver.org/) / Gestion sémantique de version,
très répandue, identifie les versions à l'aide de 3 nombres `MAJOR.MINOR.PATCH` 

Étant donné un numéro de version `MAJOR.MINOR.PATCH`, il faut incrémenter :

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

La syntaxe BNF ^[[Forme de Backus-Naur](https://fr.wikipedia.org/wiki/Forme_de_Backus-Naur)]
indique que l'on peut ajouter un identifiant de pre-release et un identifiant
de build.

::: aside
Je garde la terminologie anglaise pour les éléments de cet identifiant.
:::

## CalVer : Calendar Versioning

De façon similaire au [Semantic Versioning](https://semver.org/), l'identifiant
en [Calendar Versioning](https://calver.org/) peut comporter jusqu'à 4 parties
`MAJOR.MINOR.MICRO.MODIFIER`

Certaines parties correspondent à des **portions de dates** définies ainsi :

- **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 leur identifiant à leur convenance.

## CalVer : exemples

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

: Applications {tbl-colwidths="[33,33,33]"}

| Project                      | CalVer Format    | Examples           |
| ---------------------------- | ---------------- | ------------------ |
| C++                          | YY               | 98, 03, 11, 14, 17 |
| ECMAScript (aka JavaScript ) | YYYY             | 2015, 2020         |

: Standards {tbl-colwidths="[33,33,33]"}

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

: Libraries and Utilities {tbl-colwidths="[33,33,33]"}

::: aside
[Exemples d'utilisation du Calendar Versioning](https://calver.org/users.html)
:::

# Logiciel de gestion de version : git

## Logiciel de gestion de version

Un [logiciel de gestion de versions](https://fr.wikipedia.org/wiki/Logiciel_de_gestion_de_versions) 
(Version Control System) est un logiciel qui permet de stocker un ensemble de
fichiers en conservant la chronologie de toutes les modifications qui y ont été
apportées. 

[git](http://git-scm.com/book/) est le gestionnaire de version décentralisé le
plus répandu actuellement avec l'utilisation de forges logicielles internes
([GitLab CNRS](https://src.koda.cnrs.fr/), [GitLab INRIA](https://gitlab.inria.fr/), ...)
ou externes à nos structures ([GitHub](https://github.com/), [GitLab](https://gitlab.com/), 
[Framagit](https://framagit.org/public/projects)...).

Ces forges permettent de travailler collaborativement sur un projet.

Pour préparer la **diffusion une version d'un logiciel**, on va commencer par
étiqueter (**tag**) un point de l'historique avec un identifiant de version.

::: aside

Pour plus d'information sur [git](http://git-scm.com/book/), vous pouvez
consulter les formations de David Parsons données dans le cadre du [réseau ARAMIS](https://aramis.resinfo.org/) :

- [git débutant](https://aramis.resinfo.org/doku.php?id=ateliers:gitintro-112021)
- [git avancé](https://aramis.resinfo.org/doku.php?id=ateliers:git-avance-31032022)

ainsi que les présentations faites lors des [cafés développeur·se·s LIRIS](https://projet.liris.cnrs.fr/edp/cafes-developpeur-liris/)
:::

## git tag

[git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) permet de créer 2 types de tags :

- les **annotated tags** sont stockés en tant qu'objets dans la base de données
  git. Ils ont une somme de contrôle et contiennent le nom de l'annotateur, son
  mail, la date du tag et peuvent être signés et vérifiés par
  [GPG](https://fr.wikipedia.org/wiki/GNU_Privacy_Guard)

- les **lightweight tags** qui sont juste un pointeur sur un commit particulier

Il est **recommandé de créer des annotated tag** pour un identifiant de
version, ce qui se fait via l'option `-a` de la commande "git tag" : <br/>`git tag
-a <identifiant> -m <message>`

```{.bash}
$ git tag -a 0.1.0 -m "Nice greeting script"
$ git show -s 0.1.0
tag 0.1.0
Tagger: Françoise Conil <francoise.conil@insa-lyon.fr>
Date:   Wed Oct 18 15:51:06 2023 +0200

Nice greeting script

commit 419bfb10b5051259ac9702ac5af244461c11385e (HEAD -> main, tag: 0.1.0)
Author: Françoise Conil <francoise.conil@insa-lyon.fr>
Date:   Wed Oct 18 15:50:44 2023 +0200

    Polite greetings
```

::: {.notes}
code-line-numbers="1,3,4,5,7,9,13"
:::

---

Lorsque l'on crée un lightweight tag, on place juste une étiquette
sur un commit. Ils sont à réserver à un usage temporaire / local.

`git tag` crée un lightweight tag si les options `-a`, `-m`, `-s` sont
absentes :

```{.bash}
$ git tag 0.1.1

$ git show -s 0.1.1
commit 6a269e0f488597524b1a86db89f8a688316f8b11 (HEAD -> main, tag: 0.1.1)
Author: Françoise Conil <francoise.conil@insa-lyon.fr>
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.

```{.bash}
$ git cat-file -t 0.1.0     # annotated tag
tag

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

::: {.callout-warning title="ATTENTION"}
Il faut explicitement envoyer les **annotated tag** vers le remote avec l'option
`--follow-tags` :

`$ git push origin --follow-tags`

*en supposant que remote=origin*
:::

## git describe

La commande [git describe](https://git-scm.com/docs/git-describe) détecte le
tag annoté le plus récent accessible depuis le commit courant.

```{.bash code-line-numbers="1,2,10"}
$ 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`

```{.bash}
$ git describe 
1.2.0-8-g5fe6b80
```

---

La commande `git describe --dirty` indique que l'état de la copie de travail est
`-dirty` lorsque des fichiers suivis ont été modifiés ou quand des fichiers ont
été indexés (staged) et non enregistrés (commités) :

```{.bash}
$ touch hello.py
$ git add hello.py

$ git describe
1.2.0-8-g5fe6b80

$ git describe --dirty
1.2.0-8-g5fe6b80-dirty
```

::: {.notes}
![](images/git-tag-csvkit){fig-alt="csvkit" height="120"}
:::

# Forges

## Avec les tags sur GitHub

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

- [tags du project Flit](https://github.com/pypa/flit/tags)


## Créer une release sur GitHub 1/2

1. Dans [GitHub.com](https://github.com/), accédez à la page principale du dépôt
2. À droite de la liste des fichiers, cliquez sur **Releases**
3. Sur la page affichée, cliquez sur **Create a new Release**
4. Sélectionnez un **tag** via le menu déroulant **Choose a tag**
   - pour utiliser un tag existant, cliquez sur le tag
   - pour créer un tag, saisissez un identifiant de version et cliquez sur
     **Create new tag**
5. Si vous avez créé un tag, sélectionner la branche qui contient le code que
vous voulez "releaser" dans le menu déroulant **Target**
6. Optionnellement, sélectionnez le tag de la release précédente dans le menu
déroulant **Previous tag** au dessus du champ "Description"
7. Saisissez un titre pour la release dans le champ de saisie **Release title**

::: aside
Liste des étapes reprise de la documentation officielle : 
[Managing releases in a repository](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository?tool=webui#creating-a-release)
:::

## Créer une release sur GitHub 2/2

8. Vous pouvez lister les contributeurs en les déclarant dans la description de
la release via @mention (vous permet de sélectionner une personne dans la liste
des membres du projet). Alternativement, vous pouvez cliquer sur le bouton
**Generate release notes** pour générer le contenu (diff entre ce tag et le
précédent + pull request ?)
9. Vous pouvez ajouter des fichiers binaires par drag and drop dans la zone
sous la description
10. Vous pouvez indiquer qu'il s'agit d'une pre-release en cochant **Set as a
pre-release**
11. PAS VU : Optionnellement, cliquez sur **Set as latest release**
12. Optionnellement, créez une discussion pour cette release dans **GitHub
discussion** s'il est activé
13. Publiez la release avec **Publish release** ou enregistrer-la comme draft
avec **Save draft**

::: aside
Liste des étapes reprise de la documentation officielle : 
[Managing releases in a repository](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository?tool=webui#creating-a-release)
:::

## GitHub crée des lightweight tags

::: {.callout-warning title="ATTENTION"}
Il semble que les [tags créés par GitHub soient des lightweight tags](https://github.com/orgs/community/discussions/4924)
et non des annotated tags.

Cela peut perturber certains outils utilisés pour le packaging comme
dans l'[issue 521 de setuptools_scm](https://github.com/pypa/setuptools_scm/issues/521).
:::

## Avec les tags sur GitLab

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

- [tags du project Inkscape](https://gitlab.com/inkscape/inkscape/-/tags)

## Créer une release sur GitLab

1. Dans votre forge GitLab, allez à la page principale du dépôt
2. Dans le menu de gauche, sélectionnez **Deploy > Release** et cliquez sur
**Create a new release**
4. Via le menu déroulant **Tag name** 
   - cliquez sur un **tag** existant
   - saississez un **nouveau tag**, et cliquez sur **Create tag <tag saisi>**
5. De façon optionnelle, saisissez
   - un Titre, une Milestone, des Releases notes, l'inclusion du message du tag
     annoté
   - des "assets" complémentaires, par défaut les `zip`, un `tar.gz`, un
     `tar.bz2`, `tar` générés par GitLab, [exemples release
     Graphviz](https://gitlab.com/graphviz/graphviz/-/releases)
   - des liens vers d'autres ressources (documentation, binaires, ...)
6. Cliquez sur **Create release**

::: aside
Liste des étapes reprise de la documentation officielle : [Create a release](https://docs.gitlab.com/ee/user/project/releases/#create-a-release)
:::

# Packaging

Quelques informations, très simplifiées, pour aborder le fichier `pyproject.toml`

![](images/quand-tu-prends-un-appart-paris.jpg){height="350"}

## Python Packaging Authority

> The [Python Packaging Authority](https://www.pypa.io/) (PyPA) is a working
> group that maintains a core set of software projects used in Python packaging.
> 
> The PyPA publishes the [Python Packaging User Guide](https://packaging.python.org/),
> which is the authoritative resource on how to package, publish, and install
> Python projects using current tools.

Parmis ces documentations, [The Packaging Flow](https://packaging.python.org/en/latest/flow/)
^[Voir aussi le tutoriel [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/)]
spécifie les principales étapes du packaging.

## Principales étapes du packaging

- Avoir le **code source** du package, typiquement le checkout d'un **tag** du
  code issu d'un gestionnaire de version
- Créer un **fichier de configuration** pour décrire les **métadonnées** du package
  (nom, version, etc) et pour spécifier la manière de **générer le package**. Il
  s'agit généralemement du fichier `pyproject.toml` ^[Voir [PEP 518](https://peps.python.org/pep-0518/) 
  et [Declaring project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata)
  qui remplace la [PEP 621](https://peps.python.org/pep-0621/)]
- Exécuter l'**outil de build** pour produire une [source distribution (sdist)](https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist)
  et une ou plusieurs [built distribution (wheel)](https://packaging.python.org/en/latest/glossary/#term-Built-Distribution)
  à partir du fichier de configuration. Souvent, il n'y a qu'un [wheel](https://packaging.python.org/en/latest/glossary/#term-Wheel)
  pour un package **pur Python**
- Mettre en ligne les éléments produits sur un service de distribution de
  package (généralement [PyPI](https://pypi.org/)). 

## Historiquement

Le packaging a longtemps a reposé sur [setuptools](https://setuptools.pypa.io/en/latest/) 
et un script `setup.py`. Cela posait plusieurs problèmes dont la nécessité
d'exécuter ce script Python pour déterminer comment installer un package,
connaître ses dépendances, etc ^[Lire la série d'articles : [L'enfer des paquets Python](https://www.stella.coop/blog/00003-l-enfer-des-paquets-python-le-sac-de-noeuds)]

L’idée d'utiliser un **format déclaratif** pour la création des métadonnées de
package a été d'abord expérimentée via une solution intégrée à `setuptools` :
`setup.cfg` ^[[Aventuriers du packaging perdu (Présentation du
`setup.cfg`)](https://twidi.github.io/python-packaging-talk/fr)].

Cependant, il était également nécessaire de décorréler l'**outil d'installation**
[pip](https://pip.pypa.io/en/stable/) de l'exécution de `setuptools` avec
`setup.py`. 

Les [PEP 517](https://peps.python.org/pep-0517/) et [PEP 518](https://peps.python.org/pep-0518/)
ont défini les notions de **build frontend** (`pip`, `build`) et de **build
backend** (`setuptools`, `Flit`, `Hatch`, ...) qui doivent être spécifiées
dans le fichier `pyproject.toml`. 

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

## Build frontend

L’idée n’est pas de proposer un nouveau format de métadonnées mais de définir,
dans ce fichier texte `pyproject.toml`, les informations nécessaires pour que
le **build frontend** puisse créer un environnement de génération du package et
invoquer le **build backend** spécifié.

```{.toml}
[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"
```

Via la section `[build-system]`, le **build frontend** (`pip`, `build`) sait
qu'il a besoin d'un "environnement" Python avec `cffi`,  `packaging`, ... et
une version minimale du **build backend** `setuptool` supérieure à 61.

Il sait aussi qu'il doit appeler `setuptool` comme **build backend** pour
générer le package `pyzmq`.

## Build backends

Il devient alors possible d'utiliser d'autres **build backend** que
`setuptools` pour construire des packages. ^[[L’enfer des paquets Python : des
fichiers partout](https://www.stella.coop/blog/00009-l-enfer-des-paquets-python-des-fichiers-partout)]

L'extrait du `pyproject.toml` du projet [structlog](https://github.com/hynek/structlog)
ci-dessous indique que le **build backend** est [hatchling](https://hatch.pypa.io/latest/)
pour générer le package.

Il indique également que les packages [hatch-vcs](https://pypi.org/project/hatch-vcs/) 
et [hatch-fancy-pypi-readme](https://pypi.org/project/hatch-fancy-pypi-readme/) (en
version supérieure à 22.8.0) sont nécessaires pour le build du package `structlog`.

```{.toml}
[build-system]
requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme>=22.8.0"]
build-backend = "hatchling.build"

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

::: {.notes}
- `requires` : contient la liste des packages nécessaires pour générer le package
- `build-backend` : indique le `backend` que le `fontend` Python `build` doit
  utiliser pour générer le package : ici `hatchling`

Voir 

- `requires`, `build-backend`, `backend`, `fontend` : [The Packaging Flow](https://packaging.python.org/en/latest/flow/), 
  [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
:::

## 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`.

```{.bash code-line-numbers="6-9,12-14,21,22"}
$ 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
```

::: aside
`pip` et `build` ne sont pas installés avec `python` sur `Ubuntu`.
:::

## Génération du package par le frontend `pip`

```{.bash code-line-numbers="6,8-11,16,17"}
$ 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

```{.bash code-line-numbers="1,5,10"}
$ 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
```

## setuptools / setup.py

::: {.callout-note title="setuptools fait toujours partie des backends de build"}
L'article [Why you shouldn't invoke setup.py directly](https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html)
précise que [setuptools](https://setuptools.pypa.io/en/latest/)
est toujours maintenu et fait partie des backend utilisables (*19 oct 2021*).

C'est l'utilisation de `python setup.py <commande>` qui fait l'objet de
`DeprecationWarning` ^[Ne peut pas respecter pas les spécifications [PEP 517](https://peps.python.org/pep-0517/)
et [PEP 518](https://peps.python.org/pep-0518/) des frontends de `build`] ^[À
déterminer : quand, pourquoi et comment serait-on encore amené à utiliser un
fichier `setup.py`]

```{.bash}
$ python setup.py install
running install
/tmp/test-setuptools/.venv/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
!!

        ********************************************************************************
        Please avoid running ``setup.py`` directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.
        ********************************************************************************
```
:::

## pyproject.toml - exemple minimal 1/3

Le fichier `pyproject.toml` est un fichier texte, au format [TOML](https://toml.io/en/),
dont voici une version minimale :

```{.toml}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "example"
version = "0.1.0"
```

pour configurer le packaging du projet basique :

```{.bash}
├── pyproject.toml
└── src
    └── example
        ├── __init__.py
        └── hello.py
```

::: aside
Python Packaging User Guide :

- Guide : [Writing your pyproject.toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml),
  *page créée le 5 nov 2023*
- Tutoriel : [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata)

Scientific Python Library Development Guide : [Tutoriel Packaging](https://learn.scientific-python.org/development/tutorials/packaging/)
:::

::: {.notes}
Si la table `[build-system]` est omise, ce qui est déconseillé, le système de build va utiliser

```{.toml}
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools", "wheel"]  # PEP 508 specifications.
```

cf [PEP 518](https://peps.python.org/pep-0518/#build-system-table)
:::

## pyproject.toml - Spécifications 2/3

::: {.callout-note title="Consulter"}
- [PEP 518](https://peps.python.org/pep-0518/) Specifying Minimum Build System Requirements for Python Projects

- [Declaring project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata) (PyPA) 
  qui regroupe et remplace :

  - [PEP 621](https://peps.python.org/pep-0621/) Storing project metadata in pyproject.toml
  - [PEP 631](https://peps.python.org/pep-0631/) Dependency specification in
    pyproject.toml based on `PEP 508` qui a été intégrée à la `PEP 621`

- [PEP 508](https://peps.python.org/pep-0508/) Dependency specification for Python Software Packages 
:::

## pyproject.toml - métadonnées dynamiques 3/3

Les métadonnées dynamiques sont les métadonnées qu'un backend de build va
renseigner à l'exécution.

La liste des métadonnées qui peuvent être dynamiques dépend du backend ainsi
que la façon de récupérer dynamiquement ces métadonnées.

L'identifiant de `version` du projet est souvent récupéré dynamiquement depuis
le gestionnaire de version, généralement avec un plugin du backend.

[setuptools](https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#dynamic-metadata) 
permet de récupérer la **liste des dépendances** depuis un fichier conforme à un sous-ensemble du `requirement format` ^[Voir `pip` 
[Requirements File Format](https://pip.pypa.io/en/latest/reference/requirements-file-format/), 
fonctionne aussi avec le [`requirements.in` de pip-tools](https://pip-tools.readthedocs.io/en/latest/#requirements-from-pyproject-toml)]

::: {.callout-warning title="ATTENTION"}
Le nom du projet ne peut pas être une métadonnée dynamique. ^[[Declaring
project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#dynamic)]
:::

# PyPI

## PyPI : Présentation

[PyPI](https://pypi.org/) est le standard de fait pour le dépôt de packages
Python.

En 2022, il y a eu **35,7 milliards de téléchargements** et près de **450 000
projets** hébergés sur `PyPI` ^[[PyPI Organization: Increasing sustainability
and support](https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/#increasing-sustainability-and-support)],
représentant une augmentation annuelle de 57% du nombre de téléchargements et
de la bande passante.

En 2016, l'article de Donald Stufft indiquait que `PyPI` n'avait que **3
mainteneurs / administrateurs** et que l'essentiel des soutiens se faisaient
via la mise à disposition de services à titre gratuit par quelques entreprises.

En 2021, Dustin Ingram a publié un nouvel article intéressant sur la charge de
`PyPI`, sa gestion et ses soutiens ^[[What does it take to power the Python
Package Index?](https://dustingram.com/articles/2021/04/14/powering-the-python-package-index-in-2021/)].

::: {.callout-warning title="LIMITATIONS"}
On comprend alors les nécessaires limitations sur la taille des fichiers
uploadés (100 MiB) et sur la taille totale des projets sur PyPI (10 GiB) ^[On
peut demander une augmentation de la [taille des
fichiers](https://pypi.org/help/#file-size-limit) ou de la [taille totale du
projet](https://test.pypi.org/help/#project-size-limit)]. 
:::

::: {.notes}
Information affichée dans la gestion du projet sur `PyPI` / `TestPyPI`.
:::

## PyPI : Documentation et tests

Voici quelques sources d'information sur [PyPI](https://pypi.org/)

- [documentation utilisateur PyPI](https://docs.pypi.org/)
- [blog PyPI](https://blog.pypi.org/)
- [documentation admin/dev PyPI](https://warehouse.pypa.io/)
- [Python Packaging User Guide](https://packaging.python.org/)

### Tests

`PyPI` dispose d'une infrastructure sur laquelle on peut tester l'upload de ses
packages : [TestPyPI](https://test.pypi.org/)

## PyPI : Authentification

Afin de sécuriser le dépôt face aux packages malveillants,
[PyPI](https://pypi.org/) a mis en place l'authentification à 2 facteurs (2FA)
^[[Aide PyPI sur 2FA](https://pypi.org/help/#twofa) : L'authentification à deux
facteurs rend votre compte plus sécurisé en exigeant deux choses pour pouvoir
vous connecter : "quelque chose que vous savez" et "quelque chose que vous possédez"]
et va la rendre obligatoire d'ici fin 2023
^[[Securing PyPI accounts via Two-Factor Authentication](https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/)]

> Si vous avez choisi de configurer l'authentification à deux facteurs, vous
> devrez fournir votre deuxième méthode de vérification d'identité **pendant le
> processus de connexion**. Cela affecte **uniquement la connexion via un
> navigateur Web**, et pas (encore) la publication des paquets.<br/>
> Nous recommandons à tous les personnes utilisant PyPI de **définir au moins
> deux méthodes d'authentification à deux facteurs** et de **générer des codes de
> récupération**.

Voir la documentation sur l'utilisation d'un **périphérique de sécurité** ^[[Aide
utfkey](https://pypi.org/help/#utfkey)] ou d'une **application d'authentification**
^[[Aide TOTP](https://pypi.org/help/#totp)]

## PyPI : Activation authentification 2FA

![](images/pypi-2fa-activation.png){height="550"}

::: aside
Activation de l'authentification 2FA avec une application d'authentification
TOTP (FreeOTP)
:::

## PyPI : Jeton d'API

Il est fortement recommandé de s'authentifier avec un Jeton d'API ^[[Jeton
d'API](https://pypi.org/help/#apitoken) / API token] pour la publication d'un
package sur `PyPI`.

On peut définir 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).

On peut alors configurer l'utilisation d'un jeton dans le fichier `.pypirc`
utilisé par plusieurs outils ^[Outil de publication compte
[twine](https://twine.readthedocs.io/) ou backend comme
[Flit](https://flit.pypa.io/)] (voir la partie **backend**).

```{.ini title=".pypirc"}
[distutils]
index-servers = 
   pypi
   testpypi
   test_ntt

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

## PyPI : Trusted Publisher

La notion de [Trusted Publisher](https://docs.pypi.org/trusted-publishers/)
repose sur `OpenID Connect` pour permettre de connecter un "environnement
automatisé" de type CI ^[Continuous Integration] à `PyPI`. 

:::: {.columns}

::: {.column width="60%"}
La documentation et l'interface semblent essentiellement orientées [GitHub
Action](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
^[[PyPI doc: Adding a Trusted Publisher to an Existing PyPI Project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)]
^[[PyPA doc: Publishing package distribution releases using GitHub Actions CI/CD workflows](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows)]

::: {.callout-warning}
Je n'ai pas testé cette fonctionnalité. Lire attentivement :

- [Security model and considerations](https://docs.pypi.org/trusted-publishers/security-model/)
- [Troubleshooting](https://docs.pypi.org/trusted-publishers/troubleshooting/)

car pousser automatiquement des versions pose un certain nombre de questions. ^[[Build 
and Inspect Python Packages](https://github.com/hynek/build-and-inspect-python-package)]
:::

:::

::: {.column width="40%"}
![](images/pypi-add-trusted-publisher.png){height="450"}
:::

::::

## 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`.

::: {.callout-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`.
:::

## PyPI : Organizations

Les organisations `PyPI` sont une notion récente ^[[Introducing PyPI
Organizations](https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/)],
elles ont pour but :

- de faciliter l'identification de la provenance d'un package ^[[PyPI
  Organization Accounts](https://docs.pypi.org/organization-accounts/)]
- 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" ^[[PyPI
Organization FAQ](https://docs.pypi.org/organization-accounts/org-acc-faq/)].

# Un mot sur conda

## Présentation

[conda](https://conda.io/) est l'outil de gestion de packages pour les
installations de **Python Anaconda**.  Python Anaconda est une distribution
Python de la [société Anaconda](https://www.anaconda.com/) à destination de la
communauté scientifique, en particulier sous Windows où l'installation
d'extensions binaires est souvent difficile. 

`conda` est un outil complètement séparé des outils `pip`, `virtualenv` et
`wheel` mais il fournit plusieurs de leurs fonctionnalités en terme de gestion
de packages, de gestion d'environnement virtuel et de déploiement d'extension
binaires.

`conda` n'installe pas de package depuis [PyPI](https://pypi.org/) et ne peut
installer des packages que depuis le dépôt officiel Anaconda, ou [anaconda.org](anaconda.org) 
ou un serveur de packages local. 

Cependant, `pip` peut être installé et fonctionner en parallèle de `conda` pour
installer des packages depuis `PyPI`. [conda skeleton](https://docs.conda.io/projects/conda-build/en/latest/user-guide/tutorials/build-pkgs-skeleton.html)
permet de créer des packages conda à partir de package téléchargés depuis `PyPI` 
en modifiant leurs métadonnées.

::: {.notes}
Conda does not install packages from PyPI and can install only from the
official Anaconda repositories, or anaconda.org (a place for user-contributed
conda packages), or a local (e.g. intranet) package server. However, note that
pip can be installed into, and work side-by-side with conda for managing
distributions from PyPI. Also, conda skeleton is a tool to make Python packages
installable by conda by first fetching them from PyPI and modifying their
metadata.
:::

::: aside
Traduit de [Project Summaries - Non-PyPA projects](https://packaging.python.org/en/latest/key_projects/#non-pypa-projects)
:::

# Quelques backend pour le build de packages

![](images/xkcd_standards.png){height="350"}

Les backends se sont multipliés au point qu'il est difficile de savoir lequel
utiliser.

## backend : Flit 1/3

> [Flit](https://flit.pypa.io/en/latest/) provides a simple way to create and
> upload pure Python packages and modules to `PyPI`. It focuses on [making the easy things easy](https://flit.pypa.io/en/latest/rationale.html) 
> for packaging. `Flit` can generate a configuration file to quickly set up a
> simple project, build source distributions and wheels, and upload them to `PyPI`.

`Flit` est un outil récent ^[Cependant c'est un outil créé avant les 
[PEP 517](https://peps.python.org/pep-0517/), 
[PEP 621](https://peps.python.org/pep-0621/) et 
[PEP 660](https://peps.python.org/pep-0660/), cf 
[issue 522](https://github.com/pypa/flit/issues/522)] et simple pour générer
des packages pur Python : sans compilation de code C, sans intégration de
JavaScript, ...

`Flit` ne gère pas les dépendances.

`Flit` ne gère pas les environnements virtuels.

`Flit` ne veut pas gérer d'identifiant de version dynamique à partir des `tags
git` ^[[RFE: functionality to set __version__ from scm automatically](https://github.com/pypa/flit/issues/257)]

`Flit` a un contributeur majeur (cf "Statistiques sur le code")

::: {.notes}
À surveiller :

- 540 [Document what flit_core includes in a sdist](https://github.com/pypa/flit/issues/540)
- 522 [Discussion: future shape of the 'flit' command](https://github.com/pypa/flit/issues/522)
  Flit came about in a very different world, before various standards defined
  how packaging tools should work together (in particular, PEPs 517, 621 and
  660). A lot of design decisions I took back then don't really fit with the
  world we have now, and I don't have a strong sense of how they should be
  reconciled. So, let's have a discussion.
:::

## backend : Flit 2/3

Exemple de fichier `pyproject.toml` généré par la commande `flit init` :

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

[project]
name = "foobar"
authors = [{name = "Sir Robin", email = "robin@camelot.uk"}]
license = {file = "LICENSE"}
classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = ["version", "description"]

[project.urls]
Home = "https://github.com/sirrobin/foobar"
```

::: aside
Les métadonnées `version` et `description` sont récupérées dynamiquement :
[Flit : The pyproject.toml config file](https://flit.pypa.io/en/latest/pyproject_toml.html?highlight=dynamic#new-style-metadata)
:::

## backend : Flit publish 3/3

On peut tester l'upload de son package sur le serveur :
[test.pypi.org](https://test.pypi.org/)
^[Voir [PyPA : Using TestPyPI](https://packaging.python.org/en/latest/guides/using-testpypi/) 
et [Flit : Controlling package uploads](https://flit.pypa.io/en/latest/upload.html)]

Il est nécessaire de configurer, au préalable, l'accès au serveur
`test.pypi.org` dans le fichier `.pypirc` ^[[Flit issue 122](https://github.com/pypa/flit/issues/122)] 
ce que ne requiert pas pour un upload vers [pypi.org](https://pypi.org/)

:::: {.columns}

::: {.column width="50%"}

Publication avec un `token d'API` spécifique au projet, configuré dans le
fichier `.pypirc` :

```{.ini}
[distutils]
index-servers = 
   pypi
   testpypi
   test_ntt

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

:::

::: {.column width="50%"}

```{.bash}
$ flit publish --repository test_ntt
Found 144 files tracked in git                                          I-flit.sdist
Built sdist: dist/ntt-0.1.1.tar.gz                                 I-flit_core.sdist
Copying package file(s) from /tmp/tmpdgzv3ozw/ntt-0.1.1/src/ntt    I-flit_core.wheel
Writing metadata files                                             I-flit_core.wheel
Writing the record of files                                        I-flit_core.wheel
Built wheel: dist/ntt-0.1.1-py3-none-any.whl                       I-flit_core.wheel
Using repository at https://test.pypi.org/legacy/                      I-flit.upload
Install keyring to store passwords securely                            W-flit.upload
Server  : https://test.pypi.org/legacy/
Username: __token__
Password:
Uploading dist/ntt-0.1.1-py3-none-any.whl...                           I-flit.upload
Package is at https://test.pypi.org/project/ntt/                       I-flit.upload
...
Uploading dist/ntt-0.1.1.tar.gz...                                     I-flit.upload
...
```

:::

::::

## backend : Hatch 1/2

> [Hatch](https://hatch.pypa.io/latest/) is a unified command-line tool meant
> to conveniently **manage dependencies** and **environment isolation** for
> Python developers. Python package developers use Hatch and its **build
> backend Hatchling** to configure, version, specify dependencies for, and
> publish packages to `PyPI`. Its plugin system allows for easily extending
> functionality.

`Hatch` est un outil récent ^[Post [PEP 518](https://peps.python.org/pep-0518/)] 
mis en avant par la [Python Packaging Authority](https://www.pypa.io/) dans sa documentation.

`Hatch` intègre la gestion des environnements virtuels comme
[Poetry](https://python-poetry.org/) et <br/>
[Pipenv](https://pipenv.pypa.io/en/latest/) ^[Par contre, `Pipenv` ne génère pas de packages Python]

`Hatch` ne gère pas les dépendances.

`Hatch` a un contributeur majeur (cf "Statistiques sur le code")

## backend : Hatch 2/2

Le fichier `pyproject.toml` généré par `hatch new foobar` est assez conséquent
(157 lignes) :

```{.toml}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "foobar"
dynamic = ["version"]
description = ''
readme = "README.md"
requires-python = ">=3.7"
license = "MIT"
keywords = []
authors = [
  { name = "Sir Robin", email = "robin@camelot.uk" },
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3.7",
  "Programming Language :: Python :: 3.8",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: Implementation :: CPython",
  "Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = []

[project.urls]
Documentation = "https://github.com/unknown/foobar#readme"
Issues = "https://github.com/unknown/foobar/issues"
Source = "https://github.com/unknown/foobar"

[tool.hatch.version]
path = "src/foobar/__about__.py"

[tool.hatch.envs.default]
dependencies = [
  "coverage[toml]>=6.5",
  "pytest",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
  "- coverage combine",
  "coverage report",
]
cov = [
  "test-cov",
  "cov-report",
]

[[tool.hatch.envs.all.matrix]]
python = ["3.7", "3.8", "3.9", "3.10", "3.11"]

[tool.hatch.envs.lint]
detached = true
dependencies = [
  "black>=23.1.0",
  "mypy>=1.0.0",
  "ruff>=0.0.243",
]
[tool.hatch.envs.lint.scripts]
typing = "mypy --install-types --non-interactive {args:src/foobar tests}"
style = [
  "ruff {args:.}",
  "black --check --diff {args:.}",
]
fmt = [
  "black {args:.}",
  "ruff --fix {args:.}",
  "style",
]
all = [
  "style",
  "typing",
]

[tool.black]
target-version = ["py37"]
line-length = 120
skip-string-normalization = true

[tool.ruff]
target-version = "py37"
line-length = 120
select = [
  "A",
  "ARG",
  "B",
  "C",
  "DTZ",
  "E",
  "EM",
  "F",
  "FBT",
  "I",
  "ICN",
  "ISC",
  "N",
  "PLC",
  "PLE",
  "PLR",
  "PLW",
  "Q",
  "RUF",
  "S",
  "T",
  "TID",
  "UP",
  "W",
  "YTT",
]
ignore = [
  # Allow non-abstract empty methods in abstract base classes
  "B027",
  # Allow boolean positional values in function calls, like `dict.get(... True)`
  "FBT003",
  # Ignore checks for possible passwords
  "S105", "S106", "S107",
  # Ignore complexity
  "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
]
unfixable = [
  # Don't touch unused imports
  "F401",
]

[tool.ruff.isort]
known-first-party = ["foobar"]

[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "all"

[tool.ruff.per-file-ignores]
# Tests can use magic values, assertions, and relative imports
"tests/**/*" = ["PLR2004", "S101", "TID252"]

[tool.coverage.run]
source_pkgs = ["foobar", "tests"]
branch = true
parallel = true
omit = [
  "src/foobar/__about__.py",
]

[tool.coverage.paths]
foobar = ["src/foobar", "*/foobar/src/foobar"]
tests = ["tests", "*/foobar/tests"]

[tool.coverage.report]
exclude_lines = [
  "no cov",
  "if __name__ == .__main__.:",
  "if TYPE_CHECKING:",
]
```

## backend : Setuptools

> [Setuptools](https://setuptools.pypa.io/en/latest/index.html) is a collection
> of enhancements to the Python distutils that allow you to more easily **build
> and distribute Python distributions**, especially ones that have dependencies
> on other packages.

`setuptools` est l'outil historique pour générer des packages Python.

`setuptools` ne gère pas les dépendances.

`setuptools` n'upload pas les distributions générées sur `PyPI`, il est
conseillé d'utiliser twine.

`setuptools` ne gère pas les environnements virtuels.

::: {.callout-tip}
Les outils que j'ai expérimentés (`setuptools_scm`, `Versioneer`, `Miniver`)
pour intégrer la version git au package semblent nécessiter `setuptools`
comme **backend de build**.
:::

## backend : Poetry

> [Poetry](https://python-poetry.org/) is a command-line tool to handle
> dependency installation and isolation as well as building and packaging of
> Python packages. It uses `pyproject.toml` and, instead of depending on the
> resolver functionality within `pip`, provides its own dependency resolver. It
> attempts to speed users’ experience of installation and dependency resolution
> by locally caching metadata about dependencies.

`Poetry` est un outil apprécié par la communauté.

Créé avant les [PEP 518](https://peps.python.org/pep-0518/) et 
[PEP 621](https://peps.python.org/pep-0621/), il n'était pas en conformité avec
ces nouveaux standards. Et maintenant ?

`Poetry` intègre la gestion des environnements virtuels.

`Poetry` gère les dépendances.

`Poetry` n'est pas un projet de la [Python Packaging Authority](https://www.pypa.io/).

## backend : PDM

[PDM](https://pdm-project.org/) est mis en avant ^[[pyOpenSci Packaging
guide](https://www.pyopensci.org/python-package-guide/package-structure-code/intro.html)]
par [pyOpenSci](https://www.pyopensci.org/) une organisation ^[pyOpenSci is a
fiscally sponsored project of [Community Initiatives](https://communityinitiatives.org/) 
and is funded by the [Sloan Foundation](https://sloan.org/)] pour le peer review de package scientifique en
Open Science.

En fait, `PDM` est à la fois un **frontend** et un **backend** ce qui était le
cas de `setuptools` et pourrait prêter à confusion ^[[My User Experience Porting Off
setup.py](https://discuss.python.org/t/user-experience-with-porting-off-setup-py/37502/66)
sur [discuss.python.org](https://discuss.python.org/) ... mais c'est aussi le cas de `Flit` et `Hatch`].

Le projet semble très actif 2500 commits avec beaucoup de contributeurs.
Cependant 1789 commits / 2500 sont faits par [un unique contributeur](https://github.com/frostming).

::: {.callout-note title="PEP 582"}
`PDM` est le seul outil qui implémente la [PEP
582](https://peps.python.org/pep-0582/). Cette PEP proposait une alternative
aux environnements virtuels avec l'utilisation d'un dossier `__pypackages__`,
permettant un fonctionnement proche de [npm](https://docs.npmjs.com/about-npm).
^[[PEP 582: the future of Python packaging?](https://chriswarrick.com/blog/2023/01/15/how-to-improve-python-packaging/#pep-582-the-future-of-python-packaging)]

Est-il raisonnable d'adopter un usage sur une spécification rejetée ?
:::

## frontend / backend : une vraie séparation ?

Un post sur le forum packaging souligne que `PDM` est à la fois un `backend` et
un `frontend` ^[[My User Experience Porting Off setup.py](https://discuss.python.org/t/user-experience-with-porting-off-setup-py/37502/66)
sur [discuss.python.org](https://discuss.python.org/)].

C'est également le cas pour `Flit`, créé avant l'élaboration des spécifications
`PEP 517`, `PEP 621`, etc

Ce qui ennuyeux c'est que `flit build` ne produise pas les mêmes packages
que `python -m build` ^[[Discussion: future shape of the 'flit' command](https://github.com/pypa/flit/issues/522)]

`Hatch` peut également se comporter en `frontend` puisqu'il propose également
une [commande `build`](https://hatch.pypa.io/latest/build/).

## Comparaison des backends

Le [Scientific Python Library Development Guide](https://learn.scientific-python.org/development/),
donne un descriptif rapide de nombreux backend sur sa [page de template de projets](https://github.com/scientific-python/cookie)

D'autres tableaux comparatifs ont été réalisés ^[Cf PR [Add tools comparison
tables](https://github.com/pypa/packaging.python.org/pull/1196#issuecomment-1806879578) du Python Packaging User Guide] :

- [How to improve Python packaging, or why fourteen tools are at least twelve
  too many](https://chriswarrick.com/blog/2023/01/15/how-to-improve-python-packaging/#tooling-proliferation-and-the-python-package-authority)
  par Chris Warrick qui avait publié un article critique de
  [Pipenv](https://chriswarrick.com/blog/2018/07/17/pipenv-promises-a-lot-delivers-very-little/) en 2018
- pyOpenSci [Python packaging tools summary](https://www.pyopensci.org/python-package-guide/package-structure-code/python-package-build-tools.html#package-tool-features-table)
- [An unbiased evaluation of environment management and packaging tools](https://alpopkes.com/posts/python/packaging_tools/) 
  d'Anna-Lena Popkes interviewée dans [TalkPython n°436](https://talkpython.fm/episodes/show/436/an-unbiased-evaluation-of-environment-and-packaging-tools)

## Python Developers Survey 2022 Results

Le [Python Developers Survey 2022 Results](https://lp.jetbrains.com/python-developers-survey-2022/#PythonPackaging)
de JetBrains donne aussi des informations intéressantes sur les pratiques de la
communauté

![](images/jetbrains-python-developers-survey-2022-create-package-tool.png)

## Statistiques sur le code 1/2

```{.txt}
Statistiques de commits 

| backend    | Premier commit | dernier commit | nb commits | nb commit      | nb authors |
|            |                |                |            | per active day |            |
| ---------- | -------------- | -------------- | ---------- | -------------- | ---------- |
| Flit       |     2015-03-13 |     2023-12-09 |       1182 |            3.0 |         75 |
| Hatch      |     2021-12-29 |     2023-12-12 |        732 |            2.8 |         49 |
| setuptools |     1998-12-18 |     2023-12-06 |      14301 |            4.5 |        606 |
| PDM        |     2019-12-27 |     2023-12-11 |       2545 |            3.4 |        158 |
| Poetry     |     2018-02-20 |     2023-12-11 |       3025 |            3.0 |        515 |

Principaux contributeurs 

| Flit             | Hatch                  | setuptools        | PDM                  | Poetry            |
| ---------------- | ---------------------- | ----------------- | -------------------- | ----------------- |
| 1 : 881 (74.53%) | 1       : 641 (87.57%) | 1 : 6138 (42.92%) | 1    : 1949 (76.58%) | 1 : 1049 (34.68%) |
| 2 :  46  (3.89%) | 2 (bot) :  14  (1.91%) | 2 : 1480 (10.35%) | idem :  175  (6.88%) | 2 :  346 (11.44%) |
| 3 :  26  (2.20%) | 3       :  12  (1.64%) | 3 :  632  (4.42%) | 3    :   53  (2.08%) | 3 :  162  (5.36%) |

Date de leur dernier commit

| Flit           | Hatch                | setuptools     | PDM               | Poetry         |
| -------------- | -------------------- | -------------- | ----------------- | -------------- |
| 1 : 2023-12-09 | 1       : 2023-12-12 | 1 : 2023-12-06 | 1    : 2023-12-11 | 1 : 2022-09-18 |
| 2 : 2023-11-10 | 2 (bot) : 2023-12-01 | 2 : 2023-11-28 | idem : 2020-09-04 | 2 : 2022-06-10 |
| 3 : 2021-03-01 | 3       : 2023-04-02 | 3 : 2001-08-23 | 3    : 2021-05-05 | 3 : 2023-11-19 |
```


::: aside
- Données gitstats sur les dépôts le 2023-12-12
:::

## Statistiques sur le code 2/2

:::: {.columns}

::: {.column width="50%"}

Flit

![](images/commits_by_year_flit.png){title="Flit" height="140"}

Hatch

![](images/commits_by_year_hatch.png){title="Hatch" height="140"}

setuptools

![](images/commits_by_year_setuptools.png){title="setuptools" height="140"}

:::

::: {.column width="50%"}

PDM

![](images/commits_by_year_pdm.png){title="PDM" height="140"}

Poetry

![](images/commits_by_year_poetry.png){title="Poetry" height="140"}

:::

::::

::: {.notes}
https://quarto.org/docs/authoring/figures.html#figure-panels

layout-ncol=2 layout-nrow=3

![](images/commits_by_year_flit.png){title="Flit" height="120"}
![](images/commits_by_year_pdm.png){title="PDM" height="120"}
![](images/commits_by_year_hatch.png){title="Hatch" height="120"}
![](images/commits_by_year_poetry.png){title="Poetry" height="120"}
![](images/commits_by_year_setuptools.png){title="setuptools" height="120"}
:::

## Statistiques PyPI 1/2

![](images/python-backends-2018-2023.r.png){height="500"}

::: aside
Extraction des backends utilisés dans le fichier `pyproject.toml` grâce aux
indications et au script proposés dans l'article 
[Querying every file in every release on the Python Package Index](https://sethmlarson.dev/security-developer-in-residence-weekly-report-18)

:::

## Statistiques PyPI - log scale 2/2

![](images/python-backends-2018-2023-log-scale.r.png){height="500"}

::: aside
(1) https://gitlab.liris.cnrs.fr/fconil-small-programs/packaging/get-pypi-packages-backends
:::

# Librairies de gestion de l'identifiant de version

Il existe des librairies qui récupèrent le **tag** pour le définir
dynamiquement comme numéro de version du package.

## Intégrer l'identifiant de version au package Python 1/2

L'identifiant de version d'un package Python peut être spécifié statiquement
dans le `pyproject.toml` ou dynamiquement ^[Voir [Declaring project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/)
qui remplace la [PEP 621](https://peps.python.org/pep-0621/)]

### Statiquement

En déclaration statique, l'identifiant de version est une chaîne de caractères
dans les données globales du `pyproject.toml`.

```{.toml code-line-numbers="3"}
[project]
name = "ntt"
version = "0.1.0"
description = "Library for sport analysis"
```

### Dynamiquement

Déclaration des paramètres dynamiques dans le `pyproject.toml`.

```{.toml code-line-numbers="3"}
[project]
name = "ntt"
dynamic = ["version", "description"]
```

## Intégrer l'identifiant de version au package Python 2/2

Les modalités de récupération dépendent du backend.

### Flit

`Flit` récupère l'identifiant de version de la propriété `__version__` du
module et la description de la `docstring` du module.

``` {.bash code-line-numbers="7"}
$ tree
.
├── pyproject.toml
├── README.md
├── src
│   └── ntt
│       ├── __init__.py
│       ├── ...
```

Ces deux métadonnées sont récupérées du fichier `src/ntt/__init__.py`.

``` {.python}
"""Library for sport analysis"""

__version__ = "0.1.1"
```

## Librairies possibles

- [setuptools_scm](https://github.com/pypa/setuptools_scm)
- [hatch-vcs](https://github.com/ofek/hatch-vcs)
- [flit_scm](https://pypi.org/project/flit-scm/) basé sur `setuptools_scm`
- [Versioneer](https://pypi.org/project/versioneer/)

## setuptools_scm

[setuptools_scm](https://pypi.org/project/setuptools-scm/) extracts Python
package versions from git or hg metadata instead of declaring them as the
version argument or in an SCM managed file.

Le seul des trois qui semble compatible `pyproject.toml`

## Versioneer

[Versioneer](https://pypi.org/project/versioneer/) is a tool for managing a
recorded version number in setuptools-based python projects.

::: {.callout-warning}
Le projet qui m'a fait découvrir `Versioneer` est passé à `setuptools_scm`.
:::

## Miniver

[Miniver](https://pypi.org/project/miniver/) is a minimal versioning tool that
serves the same purpose as Versioneer, except that it only works with Git and
multiplatform support is still experimental.

## À vérifier

::: {.callout-warning}
Veillez à respecter la [PEP 440](https://peps.python.org/pep-0440/) pour
l'identifiant de version
:::

::: {.callout-tip title="À vérifier"}
Que se passe-t-il si le numéro de version déclaré dans le `pyproject.toml`
est différent du tag positionné dans le gestionnaire de version ?
:::

# Guides - documentations

## Scientific Python Library Development Guide

Le [Scientific Python Library Development Guide](https://learn.scientific-python.org/development/)
publié en juillet 2023, semble être une excellente ressource à ce jour

# Accessoirement

## pyproject.toml - la déclaration des URL

La section `[project.urls]` liste les liens à afficher pour le projet et les
étiquettes à utiliser pour ces liens. Ces étiquettes, qui ne sont pas définies
^[[Core metadata specifications](https://packaging.python.org/en/latest/specifications/core-metadata/)],
diffèrent selon les documentations, ce qui est troublant :

- URL du dépôt du code : `Repository` ^[[Declaring project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata)] ou `Source` ^[[Flit : The pyproject.toml config file](https://flit.pypa.io/en/latest/pyproject_toml.html#urls-table)] ?
On trouve aussi `URL`, `GitHub`, `GitLab`, ...

L'article [PyPI Project URLs Cheatsheet](https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet)
évoque des étiquettes trouvées via l'analyse du [code](https://github.com/pypi/warehouse/blob/70eac9796fa1eae24741525688a112586eab9010/warehouse/templates/packaging/detail.html#L20-L62) 
de [PyPI](https://pypi.org/).

Pourtant le projet [YAPF](https://pypi.org/project/yapf/0.40.2/#description) a
bien des étiquettes `Docs` et `Issues` mais PyPI n'affiche pas de lien vers les
URL. Problème de simple quote ^[[TOML String type](https://toml.io/en/v1.0.0#string)] ?
`Changelog` est pourtant affiché

::: {.notes}
L'auteur de [PyPI Project URLs Cheatsheet](https://daniel.feldroy.com/posts/2023-08-pypi-project-urls-cheatsheet)
a obtenu les étiquettes suivantes à partir du [code](https://github.com/pypi/warehouse/blob/70eac9796fa1eae24741525688a112586eab9010/warehouse/templates/packaging/detail.html#L20-L62)
de [PyPI](https://pypi.org/)

- `homepage`
- `repository`
- `changelog`
- `docs`, `documentation`
- `bugs`, `issues`, `tracker`
- `dowload`
- `sponsor`, `funding`, `donate`
- `mastodon`, `twitter`, `slack`, `reddit`, `discord`, `gitter`
:::

## Gestion des dépendances avec pip-tools 1/2

[pip-tools](https://pypi.org/project/pip-tools/) est un outil qui calcule
récursivement les dépendances spécifiées dans un `pyproject.toml`, un
`setup.py`/`setup.cfg` ou fichier texte avec une liste de packages (par
convention souvent nommée `requirements.in`).

`pip-compile` crée un environnement isolé pour le calcul des dépendances
lorsqu'on lui fournit un `pyproject.toml` :

```{.bash code-line-numbers="1,3-6"}
$ pip-compile -v pyproject.toml
Using pip-tools configuration defaults found in 'pyproject.toml'.
Creating venv isolated environment...
Installing packages in isolated environment... (flit_core >=3.2,<4)
Getting build dependencies for wheel...
Getting metadata for wheel...
Using indexes:
  https://pypi.org/simple
```

::: aside
- `pip-tools` utilise le [résolveur de dépendances de `pip`](https://pip.pypa.io/en/stable/topics/dependency-resolution/).
  Le "legacy resolver" était le résolveur interne de `pip`, le "backtracking
  resolver" serait la librairie [resolvelib](https://github.com/sarugaku/resolvelib)
  qui semble "vendorée" (intégrée) dans `pip`, voir `pip._vendor`.
- "`pip-tools` parses `pyproject.toml` metadata using `build`, not by `pip`",
  voir [issue pip-tools 1781](https://github.com/jazzband/pip-tools/issues/1781#issuecomment-1366863514)
:::

## Gestion des dépendances avec pip-tools 2/2

Cette étape ne semble pas présente lorsqu'on lui fournit un `requirements.in`

```bash
$ pip-compile -v requirements.in
Using indexes:
  https://pypi.org/simple
```

`pip-compile` génère un fichier `requirements.txt` avec les dépendances
calculées que l'on installe par `pip-sync`.


```bash
$ pip-sync requirements.txt
```

::: {.callout-warning title="ATTENTION"}
`pip-sync` installe les packages spécifiés dans le fichier `requirements.txt`.
Si des packages ont été manuellement installés dans l'environnement et qu'ils
ne sont pas présents dans le fichier `requirements.txt`, ils seront désinstallés.
:::

## Gestion des dépendances : committer requirements.txt ?

La question est posée dans une [issue pip-tools 1838](https://github.com/jazzband/pip-tools/issues/1838)

Il est certains que si les utilisateurs d'un projet ont des environnements
différents, un seul requirements.txt (ou xxx.lock) ne peut pas être appliqué
pour tous.

::: {.notes}
J'avais trouvé une pratique intéressante mais je ne sais plus où :/
:::

## Workflow de tag 1/2

![](images/do-while_run.jpg){height="350"}

::: aside
Avec ces plugin de récupération de tag dans `git`, quel est le bon workflow à adopter ?
:::

## Workflow de tag 2/2

![](images/tag-and-create-release.png){height="550"}

::: aside
À quel moment faut-il taguer dans le processus de publication de package ?
:::

## Template de projet 1/4

Le [Scientific Python Library Development Guide](https://learn.scientific-python.org/development/),
propose des templates de projet très riches à utiliser au choix avec [copier](https://copier.readthedocs.io/en/stable/),
[cookiecutter](https://cookiecutter.readthedocs.io/) ou [cruft](https://cruft.github.io/cruft/). 
Exemple avec `cookiecutter` :

```{.bash code-line-numbers="1,13,19,26"}
$ cookiecutter gh:scientific-python/cookie
  [1/9] The name of your project (package): my_scientific_package
  [2/9] The name of your (GitHub?) org (org): fconil
  [3/9] The url to your GitHub or GitLab repository (https://github.com/fconil/my_scientific_package):
  [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.): Testing the scientific templates for different backends
  [7/9] Select a license
    1 - BSD
    2 - Apache
    3 - MIT
    Choose from [1/2/3] (1):
  [8/9] Choose a build backend
    1 - Hatchling                      - Pure Python (recommended)
    2 - Flit-core                      - Pure Python (minimal)
    3 - PDM-backend                    - Pure Python
    4 - Whey                           - Pure Python
    5 - Poetry                         - Pure Python
    6 - Setuptools with pyproject.toml - Pure Python
    7 - Setuptools with setup.py       - Pure Python
    8 - Setuptools and pybind11        - Compiled C++
    9 - Scikit-build-core              - Compiled C++ (recommended)
    10 - Meson-python                  - Compiled C++ (also good)
    11 - Maturin                       - Compiled Rust (recommended)
    Choose from [1/2/3/4/5/6/7/8/9/10/11] (1): 6
  [9/9] Use version control for versioning [y/n] (y):
```

## Template de projet 2/4

Contenu du projet généré ^[Voir les [templates Scientific-Python Development Guide](https://github.com/scientific-python/cookie) et leur utilisation] :

```{.bash}
└── my_scientific_package
    ├── docs
    │   ├── conf.py
    │   └── index.md
    ├── .git_archival.txt
    ├── .gitattributes
    ├── .github
    │   ├── CONTRIBUTING.md
    │   ├── dependabot.yml
    │   ├── matchers
    │   │   └── pylint.json
    │   └── workflows
    │       ├── cd.yml
    │       └── ci.yml
    ├── .gitignore
    ├── LICENSE
    ├── noxfile.py
    ├── .pre-commit-config.yaml
    ├── pyproject.toml
    ├── README.md
    ├── .readthedocs.yml
    ├── src
    │   └── my_scientific_package
    │       ├── __init__.py
    │       ├── py.typed
    │       └── _version.pyi
    └── tests
        └── test_package.py
```

## Template de projet 3/4

Ayant demandé d'utiliser les informations du gestionnaire de version pour
l'identifiant de version. Le template utilise [setuptools_scm](https://setuptools-scm.readthedocs.io/).
Le `pyproject.toml` référence un fichier `_version.py`

```{.toml}
[tool.setuptools_scm]
write_to = "src/example/_version.py"
```

ignoré par le `.gitignore` du template

```{.txt}
# setuptools_scm
src/*/_version.py
```

Le projet contient un fichier `src/example/_version.pyi`

```{.python}
from __future__ import annotations

version: str
version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]
```

::: aside
Qu'est-ce que ce fichier `.pyi`, c'est lié au typage ? Il semblerait que oui,
ce seraient des `stub files` pour les controleurs de types statiques : [stub files dans la PEP 484](https://peps.python.org/pep-0484/#stub-files),
[stub files dans la mypy](https://mypy.readthedocs.io/en/stable/stubs.html)
:::

## Template de projet 4/4

`setuptools_scm` génère `src/example/_version.py` au build.

```{.python}
# file generated by setuptools_scm
# don't change, don't track in version control
TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import Tuple, Union
    VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
    VERSION_TUPLE = object

version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE

__version__ = version = '0.0.1'
__version_tuple__ = version_tuple = (0, 0, 1)
```

les packages ont bien l'identifiant de version correspondant au tag

```{.bash code-line-numbers="1,3,9"}
$ git tag -l -n
0.0.1           Initial release
$ git status
Sur la branche main
Fichiers non suivis:
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)
	.pre-commit-config.yaml
$ ls dist/
example-0.0.1-py3-none-any.whl  example-0.0.1.tar.gz
```

::: {.notes}
C'est trop cool et cela amène plein de connaissances et des idées de bonnes
pratiques !
:::

## Différences de génération de packages entre backends : wheel

:::: {.columns}

::: {.column width="50%"}

Flit

```{.bash code-line-numbers="7"}
├── my_scientific_package
│   ├── __init__.py
│   ├── py.typed
│   ├── _version.py
│   └── _version.pyi
├── my_scientific_package-0.0.2.dist-info
│   ├── LICENSE
│   ├── METADATA (*)
│   ├── RECORD   (*)
│   └── WHEEL    (*)
```

setuptools

```{.bash code-line-numbers="7,10"}
├── my_scientific_package
│   ├── __init__.py
│   ├── py.typed
│   ├── _version.py
│   └── _version.pyi
├── my_scientific_package-0.0.2.dist-info
│   ├── LICENSE
│   ├── METADATA (*)
│   ├── RECORD   (*)
│   ├── top_level.txt
│   └── WHEEL    (*)
```

:::

::: {.column width="50%"}

Hatch

```{.bash code-line-numbers="7-8"}
├── my_scientific_package
│   ├── __init__.py
│   ├── py.typed
│   ├── _version.py
│   └── _version.pyi
├── my_scientific_package-0.0.2.dist-info
│   ├── licenses
│   │   └── LICENSE
│   ├── METADATA (*)
│   ├── RECORD   (*)
│   └── WHEEL    (*)
```

::: {.callout-note title="(*)"}
Différences dues :

- à des hash (RECORD),
- à la présence du nom backend utilisé (WHEEL)
- à l'ordre des lignes (METADATA) à vérifier cependant. Hath et setuptools
  incluent le texte de la licence.
:::

:::

::::

::: aside
Sur la base de projets générés avec [cookiecutter](https://cookiecutter.readthedocs.io) et les
[templates Scientific-Python Development Guide](https://github.com/scientific-python/cookie)
:::

## Différences de génération de packages entre backends : sdist 1/2

:::: {.columns}

Packages générés par `python -m build` ^[[Document what flit_core includes in a sdist](https://github.com/pypa/flit/issues/540)].

::: {.column width="50%"}
Flit

```{.bash code-line-numbers="1-11"}
├── my_scientific_package-0.0.2
│   ├── LICENSE
│   ├── PKG-INFO
│   ├── pyproject.toml
│   ├── README.md
│   └── src
│       └── my_scientific_package
│           ├── __init__.py
│           ├── py.typed
│           ├── _version.py
│           └── _version.pyi
```
:::

::: {.column width="50%"}
Hatch

```{.bash code-line-numbers="2-15,17,19,22,29,30"}
├── my_scientific_package-0.0.2
│   ├── docs
│   │   ├── conf.py
│   │   └── index.md
│   ├── .git_archival.txt
│   ├── .gitattributes
│   ├── .github
│   │   ├── CONTRIBUTING.md
│   │   ├── dependabot.yml
│   │   ├── matchers
│   │   │   └── pylint.json
│   │   └── workflows
│   │       ├── cd.yml
│   │       └── ci.yml
│   ├── .gitignore
│   ├── LICENSE
│   ├── noxfile.py
│   ├── PKG-INFO
│   ├── .pre-commit-config.yaml
│   ├── pyproject.toml
│   ├── README.md
│   ├── .readthedocs.yml
│   ├── src
│   │   └── my_scientific_package
│   │       ├── __init__.py
│   │       ├── py.typed
│   │       ├── _version.py
│   │       └── _version.pyi
│   └── tests
│       └── test_package.py
```
:::

::::

::: {.notes}
Ce qui est identique entre Flit et Hatch : code-line-numbers="1,16,18,20,21,23-28"
:::

## Différences de génération de packages entre backends : sdist 2/2

:::: {.columns}

Packages générés par `python -m build`.

::: {.column width="50%"}
setuptools

```{.bash code-line-numbers="23,30-35"}
├── my_scientific_package-0.0.2
│   ├── docs
│   │   ├── conf.py
│   │   └── index.md
│   ├── .git_archival.txt
│   ├── .gitattributes
│   ├── .github
│   │   ├── CONTRIBUTING.md
│   │   ├── dependabot.yml
│   │   ├── matchers
│   │   │   └── pylint.json
│   │   └── workflows
│   │       ├── cd.yml
│   │       └── ci.yml
│   ├── .gitignore
│   ├── LICENSE
│   ├── noxfile.py
│   ├── PKG-INFO
│   ├── .pre-commit-config.yaml
│   ├── pyproject.toml
│   ├── README.md
│   ├── .readthedocs.yml
│   ├── setup.cfg
│   ├── src
│   │   ├── my_scientific_package
│   │   │   ├── __init__.py
│   │   │   ├── py.typed
│   │   │   ├── _version.py
│   │   │   └── _version.pyi
│   │   └── my_scientific_package.egg-info
│   │       ├── dependency_links.txt
│   │       ├── PKG-INFO
│   │       ├── requires.txt
│   │       ├── SOURCES.txt
│   │       └── top_level.txt
│   └── tests
│       └── test_package.py
```
:::

::: {.column width="50%"}
Hatch

```{.bash code-line-numbers="1-30"}
├── my_scientific_package-0.0.2
│   ├── docs
│   │   ├── conf.py
│   │   └── index.md
│   ├── .git_archival.txt
│   ├── .gitattributes
│   ├── .github
│   │   ├── CONTRIBUTING.md
│   │   ├── dependabot.yml
│   │   ├── matchers
│   │   │   └── pylint.json
│   │   └── workflows
│   │       ├── cd.yml
│   │       └── ci.yml
│   ├── .gitignore
│   ├── LICENSE
│   ├── noxfile.py
│   ├── PKG-INFO
│   ├── .pre-commit-config.yaml
│   ├── pyproject.toml
│   ├── README.md
│   ├── .readthedocs.yml
│   ├── src
│   │   └── my_scientific_package
│   │       ├── __init__.py
│   │       ├── py.typed
│   │       ├── _version.py
│   │       └── _version.pyi
│   └── tests
│       └── test_package.py
```
:::

::::

## Différences de génération entre flit frontend et build: sdist

:::: {.columns}

::: {.column width="50%"}
`sdist` généré par `python -m build`.

```{.bash}
├── my_project-0.0.1
│   ├── LICENSE
│   ├── PKG-INFO
│   ├── pyproject.toml
│   ├── README.md
│   └── src
│       └── my_project
│           └── __init__.py
```
:::

::: {.column width="50%"}
`sdist` généré par `flit build`.

```{.bash code-line-numbers="2-5,13-14"}
├── my_project-0.0.1
│   ├── docs
│   │   ├── conf.py
│   │   └── index.rst
│   ├── .gitignore
│   ├── LICENSE
│   ├── PKG-INFO
│   ├── pyproject.toml
│   ├── README.md
│   ├── src
│   │   └── my_project
│   │       └── __init__.py
│   └── tests
│       └── test_package.py
```
:::

::::

:::: {.columns}

::: {.column width="50%"}
```{.bash code-line-numbers="1-2"}
$ python -m build
* Creating venv isolated environment...
* Installing packages in isolated environment... (flit_core >=3.2,<4)
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating venv isolated environment...
* Installing packages in isolated environment... (flit_core >=3.2,<4)
* Getting build dependencies for wheel...
* Building wheel...
Successfully built my_project-0.0.1.tar.gz and my_project-0.0.1-py3-none-any.whl
```
:::

::: {.column width="50%"}
```{.bash code-line-numbers="1-2"}
$ flit build
Found 8 files tracked in git                                                          I-flit.sdist
Built sdist: dist/my_project-0.0.1.tar.gz                                        I-flit_core.sdist
Copying package file(s) from /tmp/tmpwbdpows6/my_project-0.0.1/src/my_project    I-flit_core.wheel
Writing metadata files                                                           I-flit_core.wheel
Writing the record of files                                                      I-flit_core.wheel
Built wheel: dist/my_project-0.0.1-py3-none-any.whl                              I-flit_core.wheel
```

Génération sur [ce squelette](https://gitlab.liris.cnrs.fr/fconil-small-programs/python/project-templates/flit-skeleton/-/tree/0.0.1) de projet.
:::

::::

## Différences de génération entre hatch frontend et build 1/2

:::: {.columns}

Aucune différence entre le `sdist` généré par `python -m build` et le `sdist`
généré par `hatch build`.

::: {.column width="50%"}

```{.bash}
├── my_project-0.0.1
│   ├── docs
│   │   ├── conf.py
│   │   └── index.rst
│   ├── .gitignore
│   ├── LICENSE
│   ├── PKG-INFO
│   ├── pyproject.toml
│   ├── README.md
│   ├── src
│   │   └── my_project
│   │       └── __init__.py
│   └── tests
│       └── test_package.py
```
:::

::: {.column width="50%"}
```{.bash}
├── my_project-0.0.1
│   ├── docs
│   │   ├── conf.py
│   │   └── index.rst
│   ├── .gitignore
│   ├── LICENSE
│   ├── PKG-INFO
│   ├── pyproject.toml
│   ├── README.md
│   ├── src
│   │   └── my_project
│   │       └── __init__.py
│   └── tests
│       └── test_package.py
```
:::

::::

:::: {.columns}

::: {.column width="50%"}
```{.bash code-line-numbers="1-2"}
$ python -m build
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (hatchling)
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (hatchling)
* Getting build dependencies for wheel...
* Building wheel...
Successfully built my_project-0.0.1.tar.gz and my_project-0.0.1-py3-none-any.whl
```
:::

::: {.column width="50%"}
```{.bash code-line-numbers="1-3"}
$ hatch build
[sdist]
dist/my_project-0.0.1.tar.gz

[wheel]
dist/my_project-0.0.1-py3-none-any.whl
```
:::

::::

## Différences de génération entre hatch frontend et build 2/2

`hatch build` affiche message transitoirement sur une ligne `... building environment`.

Si on déplace les packages qui ont été générés dans `dist`, et que l'on exécute
`hatch build` à nouveau cette ligne n'est plus affichée comme si
l'environnement était encore actif quelque part et avait une trace du type
"rien n'a changé" (les fichiers sont regénérés à l'identique).

On ne sait pas où cette information et ces données sont stockées.

## changelogs : towncrier

[towncrier](https://towncrier.readthedocs.io/en/stable/index.html) semble
servir à créer des `changelogs`.

::: {.callout-note title="À ÉTUDIER"}
Used by Twisted, pytest, pip, BuildBot, and attrs, among others.
:::

## Dépôt GitHub de fichiers `.gitignore`

[.gitignore Python](https://github.com/github/gitignore/blob/main/Python.gitignore)

