# Distribuer un projet en python

La plus simple manière de partager un projet écrit en python est de le paqueter dans une archive zip, et de laisser chacun extraire et installer votre projet là où c'est nécessaire. Ce procédé fastidieux a été automatisé par des librairies tierces, intégrées dans la librairies standard de python dès Python 1.6. Parmi tous les outils qui sont apparus pas la suite on peut citer distutils, easy_install, pkg_resources, setuptools, pip, buildout... La manière de paqueter son projet et de le distribuer ne cesse d'évoluer, ce livre essaie de vous aider à vous repérer dans cet univers. Une solide connaissance du langage python est nécessaire avant de continuer, vous pouvez lire ce livre pour cela : Programmation Python.

## Module tiers

Dans le vocabulaire python, le projet que vous chercher à paqueter et distribuer est appelé un « module tiers » (third-party module). Quel que soit l'outil que vous utilisez (easyinstall, pip...), l'endroit où vos fichiers s'installeront doit être trouvable par le module site de python, situé à Lib/site.py). Pour simplifier, ce fichier va mettre à jour la propriété sys.path avec le dossier site-packages pour que vous puissiez simplement import module tiers dans vos projets, et que ceux-ci soient centralisés dans un unique dossier utilisable par tous vos projets. C'est très utile pour les librairies lourdes. Pour savoir comment site.py va altérer votre environnement, tapez simplement python -m site. Pour empêcher python d'exécuter ce script, lancez-le avec l'option -S, c'est à dire python -S, cela vous laisse loisir de modifier l'environnement d'exécution puis d'importer vous même ce module via import site, sachant qu'il va faire beaucoup de lectures de dossiers et peut ralentir le démarrage de votre script python.
Pour résumer après avoir exécuté site.py, on peut simplement importer tout module tiers dans ses projets, sans avoir besoin d'avoir ces dits modules dans la même arborescence que le projet en question, mais au lieu de cela dans un environnement virtuel, dans un dossier utilisateur (/home sur Unix, AppData sur Windows…), ou dans un dossier de python lui même.

## Environnement virtuel

Non seulement d'inclure les site-packages, site.py essaie de détecter si l'instance de python courante s'exécute dans un « environnement virtuel » (dit aussi venv, pour virtual environment). Il détermine cela si il existe un fichier pyenv.cfg dans le répertoire immédiatement au dessus de l'exécutable python en cours. Si c'est le cas, le fichier site-packages de l'environnement virtuel est placé tout en haut de la liste sys.path afin que les imports soient en priorité cherchés dans cet environnement, puis dans le site-packages de l'utilisateur en recours, et le site-packages de l'installation python en deuxième recours.
Toutefois si le fichier pyenv.cfg contient la ligne include-system-site-packages = false, les site packages de l'utilisateur et de l'environnement sont supprimés du sys.path. Un import de module ne peut alors réussir que si il existe bien dans l'environnement virtuel ou bien si il s'agit d'un core-lib de python (random, urllib, ...). Cette configuration est indispensable si on veut s'assurer que l'environnement de travail nécessaire au fonctionnement de son module soit reproductible au mieux par les tierces personnes avec qui vous partagerez les plans de votre environnement virtuel.
La manière dont vous décrivez ces environnement est propre à chaque outil, nous décrirons plus loin celui de pip.

## Le paquetage

Pour utiliser votre module, il faut donc l'inclure dans un paquetage qui contient en outre les noms d'autre modules tiers que votre module utilise dans un fichier texte, ce fichier peut être utilisé pour établir un arbre de dépendances, et il est déconseillé de le modifier à la main mais de laisser pip freeze s'en occuper.
Maintenant comment construire ce paquetage ? Encore une fois le faire à la main peut être fastidieux, surtout si votre module contient beaucoup de fichiers, des fichiers non en python, des fichiers que vous ne souhaitez pas partager, etc. Python contient un module spécialement dédié à cette fin, nommé « distutils ». Si vous êtes curieux, vous le trouverez dans Lib/distutils, et sa documentation est à l'adresse install/index.html de la documentation officielle. On voit que la finalité première de ce module est de produire des paquetages dans le sens de linux, c'est à dire des fichier .rpm. En ce qui nous intéresse, dans un premier temps, c'est de créer une archive .zip, .tar.gz, que pip install est capable de lire (si vous tapez python -m pip install --help, vous verrez qu'en outre de lire un fichier de requirements il sait lire une archive zip, en sus de savoir chercher un module sur pypi, d'un dépôt git où d'un simple chemin vers une arborescence locale). Ce que pip va faire dans tous les cas, plutôt que de recopier tout le contenu du répertoire, déport distant, archive, etc. est d'exécuter un fichier dont le nom est normalisé : setup.py avec l'argument install, c'est ce fichier qui va donc chapeauter ce qui est copié ou non dans le dossier site-packages via pip.
Ce fichier peut être utilisé par soi même de multiples manières : construire une archive, un paquetage linux, voire même un installeur Windows (même si cela est déconseillé de nos jours et non plus officiellement supporté), mais également des outils tiers comme pip.
Dans sa forme la plus simple le fichier setup.py contient un import de la librairie setuptools, et un appel a la fonction setup de celle-ci qui lit le contenu de la ligne de commande et exécute les taches nécessaires à la copie de fichiers, la compilation de modules, de paquetage dans une archive, etc.
Cet exemple introduit setuptools, qui est un module tiers à python que vous devez installer au préalable via python -m pip install setuptools et qui est un sur-ensemble de setuptools; c'est à dire qu'il permet de faire tout ce que distutils peut faire, mais en y ajoutant des fonctionnalités propres. Distutils existe depuis Python 1.5, rétro compatible jusque Python 1.6, Setuptools a vu le jour au début des années 2000, dans la même idée et de manière concurrente à zc.buildout, au final setuptools a été retenu comme outil officiel. Ces extensions ont la particularité d'importer la fonction setup de distutils et de l'appeler avec les mêmes arguments qu'ils ont reçu en entrée, on peut résumer le fonctionnement de ces outils ainsi.
De la même manière les fichiers qui exécutent les commandes (setup, build, sdist...) importent le fichiers de distutils et l'appellent tout en y ajoutant leurs propres fonctionnalités. Tout ce que l'on peut faire avec distutils, on peut le faire avec setuptools (ou presque). Si l'objectif de distutils est de produire des bundles installables à telle ou telle plateforme, celui de setuptools est assez multiple, mais principalement de produire des eggs spécifiques à python, et couplé avec l'utilitaire easy_install, de simplifier l'installation et la désinstallation (ce que distutils ne permet pas de faire) de paquetages en python, et distribuables sans nécessité de build. Mais pour bien les comprendre il faut comprendre comment distutils fonctionne et comment il est possible de l'étendre.

## Distutils

Aux débuts de python, jusque la version 1.5, lorsqu'on voulait partager son projet, si on voulait le rendre accessible sur Linux on créait un paquetage .rpm, .deb…, sous Windows on créait un installeur .msi, .exe. Cela demandait du temps et un niveau de compétence plus élevé que celui d'un débutant. De même l'installation diffère pour chaque projet et la mise en place de l'environnement se faisait qui avec des scripts shell, qui avec des instructions à suivre à la lettre… Les plus pressés incluaient simplement leur projet dans un .zip, mais alors il fallait faire attention à ne pas y mettre des fichiers temporaires, en cache, etc. Si on voulait inclure des extensions en C il ne fallait pas oublier de les compiler avant de les inclure. Toutes ces tâches répétitives ont été automatisées par distutils.
Pour la tâche de paqueter et de compiler distutils propose la commande bdist, et pour celle de produire une archive zip distutils propose la commande sdist ainsi qu'un format d'inclusion et d'exclusion de fichiers via une syntaxe de globing et un fichier MANIFEST. La tâche de l'installation est simplifiée car celle-ci passe par une commande universelle python setup.py install. Ce fichier setup étant en python il peut alors installer les dépendances, vérifier les prérequis, et tout ce qui demandait de la minutie et un travail manuel auparavant.
Pour utiliser distutils, votre projet doit contenir un fichier setup.py, qui contient le code python nécessaire à l'installation de votre module. On verra plus tard que pour automatiser cette installation il est préférable que ce fichier se trouve à la racine de l'arborescence du projet et s'appelle bien setup.py. Cette installation, doit aboutir à la copie des fichiers nécessaires dans le répertoire site-packages le plus pertinent.
La machinerie de distutils se met en place en appelant la fonction setup de distutils.core. Cette fonction lit la ligne de commande utilisée pour invoquer le script puis exécute la commande appelée. python ./setup.py toto --arg1=2 appelle la commande toto avec l'argument arg1 valant 1. Un fichier setup.cfg peut contenir des valeurs d'arguments par défaut pour chaque commande. Si il est ainsi
La commande toto s'exécute avec arg1=2 at arg2=1, la ligne de commande prévaut sur le fichier de configuration.
Ne reconnaissant pas la commande toto un message d'erreur s'affiche avec la liste des commandes acceptées. Leur code source peut se trouver dans Libs/distutils/commands. Si vous lisez ces fichiers, et notamment la méthode run, vous remarquerez que beaucoup de commandes en appellent d'autres. Par exemple install appelle build, build peut appeler build_ext, build_py..., que chaque commande a des arguments différents qui peut altérer la course de l'installation, et des valeurs par défaut.
Les principales commandes sont build, install, sdist, bdist et bdist_*.
La commande build copie les fichiers python dans un répertoire, par défaut build/lib*, compile les modules en C/C++ et les copie également, par simplicité lorsqu'on parle de librairie plus loin dans ce cours, c'est de ce dossier qu'on parlera. La commande install recopie tout ce qui se trouve dans le répertoire peuplé par build dans le dossier site-packages, la commande sdist produit une archive zip avec tout ce qu'il faut pour que la commande bdist réussisse. Sachant cela il apparait que la fonction install peut fonctionner à partir de la commande build, sdist et bdist, la commande bdist à partir de la commande build et sdist, mais les commandes build et sdist ne peuvent pas fonctionner à partir d'une archive faite par bdist. setup/build fonctionnent à partir d'un répertoire local, sdist/bdist servent à partager le répertoire local, bdist avec le minimum vital (fichiers pyc, pyd, ...), et sdist avec les tests, fichiers source (py, pyx, c, c++, ...).
Notre objectif premier était de produire un répertoire zip facilement installable avec pip, donc doit-on utiliser ici sdist ou bdist? Cela dépend du contenu de notre paquetage, si il ne contient que des fichiers en python, les deux commandes sont assez similaires, mais si ils contients du code en cython, ou des modules C/C++ natifs, alors dans le cas de sdist les clients de votre librairie doivent posséder un compilateur C (qui occupe un espace disque jusque 30 giga-octets) et les librairies python de dev, par défaut non installées. Si vous optez pour bdist, les modules binaires doivent être compatible avec chaque machine, c'est à dire qu'une distribution bdist faite en 64 bits ne fonctionnera pas sur une machine 32 bits, un module fait avec une librairie python 3.7 probablement ne fonctionnera pas dans un environnement 3.6 (même si l'usage de l'ABI stable de python peut mitiger cela), une librairie construite sous mac, windows ou linux ne fonctionnera pas sur les autres machines. Nous verrons cette problématique bien plus tard car elle nécessite une machinerie complexe hors de notre propos, mais si vous êtes curieux, les mots-clés sont soit cross-compilation, soit continous integration, et nous concentrons sur sdist dans la suite du livre, et sur la forme de bdist la plus simple : celle faite par et pour soi.

### Build

Qu'on utilise la commande install, sdist ou bdist, la commande build est exécutée dans tous les cas, pour qu'elle sache quoi mettre dans la librairie, on utilise l'argument py_modules et package de setup. Ceux-ci ont d'abord été mutuellement exclusifs, avant qu'il devienne possible de les utiliser en même temps. py_modules accepte une liste de chaines de caractères, qui doivent correspondre à la manière dont les fichiers seront importés, par exemple un fichier toto.py est importé ainsi
Si vous avez un autre fichier dans un sous-répertoire a > b, celui-ci doit contenir un fichier __init__.py, et vous le déclarez ainsi
Cela inclut automatiquement le fichier __init__, pas besoin d'écrire setup(py_modules=['toto', 'a.b.toto', 'a.b.__init__]). Ces noms d'import doivent obligatoirement référencer des fichiers qui existent et se terminent par .py, de préférence se trouvent dans un répertoire contenant un fichier __init__ (sinon un warning s'affiche). py_modules n'est pas compatible avec des fichiers non python (readme, ...) ou compressés (pyo, pyc) ou des paquetages PEP420.
L'option packages est plus complexe à écrire, mais permet d'inclure tout type de fichier et d'utiliser une syntaxe de globing pour inclure rapidement des répertoires entiers de fichiers dans la librairie. Si on veut inclure les mêmes fichiers qu'avec py_modules, mais en plus un README, et des fichier en python compilés et de typage dans le répertoire a > b, on écrit
packages prend une liste de noms de répertoires à chercher pour y trouver des fichiers en python et les inclut automatiquement, Si on veut ajouter d'autres fichiers que des scripts en python, on doit utiliser package_data qui prend un dictionnaire de champs nom de package -> liste de globs à appliquer. Le champ au nom vide '' s'applique à tous les packages et à la racine du projet. Le format packages n'inclut que des fichiers situés dans des packages, pour inclure le fichier toto à la racine du projet on doit utiliser py_modules, au final
On remarque que si le répertoire a>b contient un fichier README, il sera inclus aussi. Seul les règles de la racine s'appliquent à tous les sous-paquetages, si vous définissez un sous-paquetage a.b.c, les règles propre à a.b ne s'appliquent pas à a.b.c.
Si vous ne voulez pas inclure certains scripts en python, par exemple des scripts contentant des données locales dans votre paquetage, le plus simple est de les mettre dans un répertoire séparé non listé dans l'entrée packages, mais vous pouvez aussi utiliser py_modules juste pour le répertoire où ces fichiers se trouvent, mais cela peut être dangeureux avec setuptools et son outils de découverte automatique.
package_data est une fonctionnalité créé par setuptools et importée dans distutils depuis python 2, setuptools fonctionne différemment vis à vis de ce paramètre et peut l'ignorer complètement, nous verrons avec sdist que la présence d'un fichier MANIFEST.in peut être mutuellement exclusif avec ce paramètre.

### sdist

python setup.py sdist inclut tous les fichiers spécifiés précédemment en plus de
* setup.py
* les fichiers de readme
* les fichiers de test
La liste à jour et complète est dans la documentation (lien). Un fichier PKG-INFO est également ajouté qui contient des informations utiles pour ensuite créer des packages de type rpm, il contient le nom du projet, sa version, un résumé expliquant ce qu'il fait, un lien vers un site web, un nom d'auteur, des informations de license et une plateforme préférée, toutes ces informations sont passées via setup, par exemple
Un fichier MANIFEST.in peut indiquer des fichiers supplémentaires à inclure dans la distribution, mais si ces fichiers ne sont pas spécifiés dans package_data, ils ne seront pas dans les distributions binaire et absents de la feuille de route, le fonctionnement de build ignore en effet les fichiers de manifest, qui ne servent que pour sdist, l'idée est que les extras comme la documentation peut être consultés via la ligne de commande (help), et n'ont pas besoin d'être dans le répertoire exécutable de python. setuptools change cette interaction, et distutils lui même été changé à de nombreuses reprises, si bien que de nombreuses choses non concordante ont été écrits à ce sujet.

### bdist

Les fonctionnalités de bdist de distutils est largement obsolète, par défaut il produit un installeur qui copie les fichiers vers des chemins absolus, si bdist a été exécuté dans /usr/local/python, alors install va copier votre librairie vers ce même dossier pour les clients. Ce n'est pas compatible avec venv, ou tout système non unix. Autrement bdist peut aussi produire des paquetages rpm, des installeurs windows de type .msi ou .exe qui sont eux aussi largement obsolètes. C'est avec setuptools et le format egg que la commande bdist peut être réellement utilisée.
Une archive bdist de base ne contient pas le script setup.py, de PKG-INFO, et ignore les MANIFEST et autres fichiers inclus par sdist, c'est en effet à l'outil final (apt-get, .msi, ...) que revient la tâche de faire l'installation. Il produit cependant un fichier d'extension *.egg-info qui contient les mêmes données que PKG-INFO.

## Setuptools

Setuptools permet en utilisant le format binaire bdist_egg, un répertoire de modules python centralisé pypi et l'utilitaire easy_install, de télécharger depuis une invite de commande un projet en python quel que soit la plateforme, contrairement aux anciens format .rpm ou .exe non spécifiques à python. Par exemple easy_install Pil télécharge ce module, le décompresse puis l'installe suivant les spécifications du format egg. Pour l'utiliser on remplace from distutil.core import setup par from setuptools import setup dans le fichier setup.py, la génération d'un egg se fait ainsi
Une fois l'egg produit, on peut l'installer en faisant pointer easy_install dessus, easy_install d:chemin/vers/le/fichier/egg, ou l'uploader sur pypi, le champ name de setup est utilisé comme clé doit être unique et ne pas être déjà utilisé, ce qui permet à tout le monde d'installer son module avec easy_install <name>. La documentation du format egg peut se trouver ici et ici, ce format est toutefois obsolète.

### Découverte de packages

Avec distutils, MANIFEST.in ne sert que pour les distributions source, et package_data pour les installations. Setuptools ajoute une option booleene include_package_data de sorte que le fichier MANIFEST.in serve pour les deux, le paquetage et l'installation, ce qui déprécie package_data. L'usage de package_data a toutefois été rétabli plus tard pour permettre de n'installer des fichiers que lors d'une installation et non lors d'une distribution source, si on ne souhaite pas passer par sdist mais par git par exemple, il devient aussi possible d'exclure des fichiers englobés par package_data, de la même manière que distutils via MANIFEST.in, avec l'argument exclude_package_data. L'usage exclusif de MANIFEST.in est généralement recommandé.
Il fut ensuite possible d'inclure dans les distributions source tout le contenu suivi par cvs ou svn, à condition qu'il n'y ait pas de fichier MANIFEST.in à la racine du projet. Pour git cette fonctionnalité est assurée par le greffon setuptools_scm, qu'on active ainsi
Les formats binaires continuent d'ignorer le manifest et les plugins de gestion de version, et ne lisent que l'attribut packages, pour la simplifier on l'écrit
find_namespace_packages sert à inclure les sous-paquetages PEP420.

## Pip

Pip est l'outil recommandé par python pour installer rapidement un paquetage en python et remplace easy_install. Il est désormais inclus par défaut dans les distributions python, si vous ne l'avez pas, vous pour l'obtenir avec la commande python get-pip.py, vous pouvez soit taper taper la commande pip directement dans une invite de commande, soit, ce qui est préférable le lancer via python python -m pip. Si vous lancez pip sans argument vous verrez une liste de commandes qu'il peut exécuter, celles qui nous intéressent sont install et freeze. freeze affiche la liste des modules tiers dans le site-packages en cours, dans un format que la commande pip install peut comprendre. La manière la plus pratique pour conserver une trace de ces noms de modules est la syntaxe suivant python -m pip freeze > requirements.txt. Ce nom de fichier est normalisé et il est recommandé de toujours l'utiliser. Un utilisateur tiers pourra alors imiter votre environnement simplement en tapant la commande python -m pip install -r requirements.txt, de préférence en ayant créé et activé un environnement virtuel au préalable, dans le même souci d'avoir un environnement reproductible, et de ne pas polluer la commande pip freeze si il souhaite lui aussi participer à votre module. Pour que cela soit possible il faut que le fichier requirements.txt – qui n'a de sens que pour pip – soit inclus avec votre module, on parle alors plutôt de « paquetage », en anglais package, car notre projet contient autre chose qu'un simple script en python.
Pip a contribué au format wheel (.whl) pour remplacer le format egg.

## Pep517

Dans les années 2010 des outils qui ne sont pas des extensions de distutils voient le jour et ont pour objectif de remplacer pip, on peut citer bento (obsolète), flit ou poetry. Flit est le seul outils qui permette la création de distributions binairement reproductibles, quand à poetry il permet d'une simple commande de créer un environnement virtuel, télécharger un wheel et de l'installer. Ces nouveaux outils en plus d'utiliser le format wheel savent lire le format sdist dirigé par MANIFEST.in. Ils n'assurent pas toutes les fonctionnalités rendues possibles par setuptools, notamment la commande setup.py develop (ou assimilé). Il est possible de déclarer son paquetage de plusieurs manières grâce à un fichier pyproject.toml de sorte qu'il est possible d'accommoder plusieurs systèmes d'installation. On peut forcer pip à utiliser le nouveau ou l'ancien système avec l'option --pep517 et --no-pep517 selon qu'on veuille les fonctionnalités de setuptools ou bien celles des nouveaux systèmes, par exemple pip install --editable --no-pep517.

## Python 3.10

Depuis Python 3.10, l'objectif est de déprécier complètement le module distutils, et il est prévu que la librairie ne soit plus incluse dans la librairie de python à partir de la version 3.12. Une PEP y est consacré ; PEP 0632. python -m build replacera bdist*.

## Terminologie

* Projet, paquetage, distribution : dans le vocabulaire il s'agit d'un module tiers, on l'utilise dans ce livre dans le sens de linux, comme produit fini, archive installable qui contient éventuellement des sous paquetages en python (appelés simplement paquetage pour python).
* Sous-paquetage, sous-module : dans le vocabulaire python il s'agit d'un paquetage ou package.
* Distribution source : Une archive contenant le nécessaire au développement, il s'agit de l'équivalent d'un git clone, contenu dans une archive. Les outils mentionnés dans le livre existaient avant que les systèmes de gestion décentralisés soient populaires.
* Distribution binaire : Un fichier utilisable par des installeurs finaux, contient le minimum nécessaire à l'utilisation de son projet.
* Commande : option passée à un outil, par exemple pip install, install est la commande.
* Paramètre : option passée à une commande ou à l'utilitaire général. Par exemple pip --toto install --tata, toto est un paramètre pour pip et tata un paramètre pour la commande install.
* Outil, outillage, les commandes de premier niveau, c'est à dire pip, easy_install, ou python setup.py
* Arbre de dépendances : schémas descriptif et abstrait de python qui permet à l'outillage de connaitre les dépendances de son projet et des les installer récursivement.
* Manifest, feuille de route : fichier qui décrit les étapes à suivre de manière abstraite à python pour installer ou paqueter une distribution source, et permet ainsi sa désinstallation automatique et suivant les étapes à l'envers.

## Ressources

(enlever les espaces)
* Ressources officielles
** packaging.python.org/ Guides et tutoriaux à jour
** www.pypa.io/en/latest/ Spécifications
** docs.python.org/fr/3/distributing/index.html Documentation
* Documentation historique de distutils
** docs.python.org/fr/3/distutils/index.html
** Anciennes : docs.python.org/release/1.6/inst/inst.html, docs.python.org/release/1.6/dist/dist.html
* Documentation de setuptools : setuptools.readthedocs.io/en/latest/index.html
* Documentation de pip : pip.pypa.io/en/stable/

## Références

* ↑ http://www.buildout.org/en/latest/topics/history.html/
* ↑ https://setuptools.readthedocs.io/en/latest/deprecated/distutils-legacy.html
* ↑ https://docs.python.org/fr/3/c-api/stable.html
* ↑ https://docs.python.org/3/library/glob.html#glob.glob
* ↑ https://setuptools.readthedocs.io/en/latest/userguide/datafiles.html
* ↑ https://setuptools.readthedocs.io/en/latest/userguide/datafiles.html
* ↑ https://docs.python.org/fr/3/distutils/sourcedist.html#specifying-the-files-to-distribute
* ↑ https://github.com/pypa/setuptools/commit/4cd66c4147bef3ee8096f7161d407fb37582f1c9
* ↑ https://github.com/pypa/setuptools/commit/d98923012
* ↑ https://docs.python.org/fr/3/distutils/commandref.html#sdist-cmd
* ↑ https://reproducible-builds.org/
* ↑ https://docs.python.org/3.10/whatsnew/3.10.html