.. highlight:: console
.. _packaging:
Packaging
#########
.. contents:: Table des matières
:local:
..
1 *Package* non-initialisé
2 *Package* avec initialisation
2.1 Scripts
3 Configuration
3.1 Point d'entrée
4 Installation locale
5 Distribution
5.1 Code source
5.2 Dépôt git
5.3 Dépôt PyPI
3 Configuration
3.1 Points d'entrée
4 Installation locale
5 Distribution
5.1 Code source
5.2 Dépôt git
5.3 Dépôt PyPI
.. Avertissement:: le *packaging*, la distribution et l'installation
des *packages* sous Python fait encore l'objet de développements
(et d'une certaine confusion, voir p.ex. les `Packaging PEPs
`_ et la `Python
Packaging Authority `_, ainsi que cet
`historique
`_...).
Je présente ici une solution raisonnablement « pérenne » basée sur
:pypi:`setuptools` et `pyproject.toml`, mais qui peut être amenée à
évoluer.
.. index::
module
package
Le *packaging* facilite la distribution et l'installation de vos bibliothèques
auprès de vos collaborateurs ou de toute la communauté.
Les bibliothèques peuvent être de deux types:
:doc:`python:tutorial/modules`
Un module est constitué d'un seul fichier, facilement distribuable et
installable.
:ref:`python:tut-packages`
Un *package* regroupe un ou plusieurs modules dans une arborescence dédiée.
*Package* non-initialisé
========================
Un *package* peut n'être qu'une simple collection de modules::
pkg/
├─ mod1.py
├─ mod2.py
└─ subpkg/
└─ mod.py
auquel cas les modules doivent être explicitement importés [#pythonpath]_:
>>> import pkg.mod1
>>> import pkg.subpkg.mod
Les (sous-) *packages* n'étant pas initialisés, ils ne contiennent rien eux-mêmes:
>>> import pkg
>>> pkg.mod1
AttributeError: module 'pkg' has no attribute 'mod1'
*Package* avec initialisation
=============================
.. index:: pair: packaging; __init__.py
Dans les cas plus complexes, les répertoires contiennent des fichiers
d'initialisation `__init__.py` qui sont automatiquement interprétés à
l'importation. Les *packages* initialisés étant généralement destinés à être
distribués, leur structure prend une forme conventionnelle, p.ex., pour un
package nommé `pyYC`:
.. note:: Par facilité, j'utilise ici une structure *flat*, voir cependant `src
layout vs flat layout
`_
pour une autre structure éventuellement préférable.
.. code-block:: bash
pyYC/ # Répertoire racine du package pyYC (nom libre)
│
├─ pyyc/ # Répertoire source (package principal)
│ │
│ ├─ __init__.py # Initialisation du package lors de 'import pyyc'
│ │
│ │ "Documentation du package pyyc."
│ │
│ │ __version__ = "0.6" # à màj régulièrement
│ │ print(f"Initialization {__package__} v{__version__}")
│ │
│ │ from . import mod # import relatif du module mod
│ │ from . import subpkgA # import relatif du sous-package subpkgA
│ │ from . import subpkgB # import relatif du sous-package subpkgB
│ │
│ ├─ mod.py # Module top-level
│ │
│ │ "Documentation du module mod."
│ │
│ │ __all__ = ['version'] # limite la portée de "import *" à l'attribut 'version'
│ │ version = "top-level module"
│ │ print(f"Initialization {__name__!r}: {version}") # NO PRINT in a true module!
│ │ ...
│ │
│ ├─ subpkgA/ # Sous-répertoire pour le sous-package subpkgA
│ │ │
│ │ ├─ __init__.py # Initialisation du sous-package ('import pkg.subpkgA')
│ │ │
│ │ │ "Documentation du sous-package subpkgA."
│ │ │
│ │ │ version = 'sub-package A'
│ │ │ print(f"Initialization {__package__!r}: {version}")
│ │ │
│ │ │ __all__ = ['modA1', 'modA2'] # modules importés par 'import *'
│ │ │
│ │ │ for _mod in __all__: # imports programmatiques ≈ from __name__ import _mod
│ │ │ __import__(__name__ + '.' + _mod, fromlist=[None])
│ │ │
│ │ ├─ modA1.py # module modA1
│ │ │
│ │ │ "Documentation du module modA1."
│ │ │
│ │ │ version = 'sub-package A module A1'
│ │ │ print(f"Initialization {__name__!r}: {version}")
│ │ │ ...
│ │ │
│ │ └─ modA2.py # module modA2
│ │
│ │ "Documentation du module modA2."
│ │
│ │ version = 'sub-package A module A2'
│ │ print(f"Initialization {__name__!r}: {version}")
│ │ ...
│ │
│ └─ subpkgB/ # Sous-répertoire pour le sous-package subpkgB
│ │
│ ├─ __init__.py # Initialisation du sous-package ('import pkg.subpkgB')
│ │
│ │ "Documentation du sous-package subpkgB."
│ │
│ │ version = 'sub-package B'
│ │ print(f"Initialization {__package__!r}: {version}")
│ │
│ │ from .modB import * # import local
│ │
│ └─ modB.py # module modB
│
│ from ..subpkgA import modA1 # import relatif
│
│ version = f'sub-package B module (incl. {modA1.version})'
│ print(f"Initialization {__name__!r}: {version}")
│ ...
│
├─ docs/ # Répertoire de documentation
├─ tests/ # Répertoire de tests
├─ LICENSE # Fichier de licence (voir https://choosealicense.com/)
├─ README # Fichier de présentation sommaire
├─ pyproject.toml # Fichier de configuration pour le packaging
└─ ...
.. warning:: Contrairement à ce qui est fait ici pour des raisons
pédagogiques, il est fortement déconseillé de faire des `print`
dans l'initialisation d'un package!
>>> import pyyc
Initialization pyyc v0.6
Initialization 'pyyc.mod': top-level module
Initialization 'pyyc.subpkgA': sub-package A
Initialization 'pyyc.subpkgA.modA1': sub-package A module A1
Initialization 'pyyc.subpkgA.modA2': sub-package A module A2
Initialization 'pyyc.subpkgB': sub-package B
Initialization 'pyyc.subpkgB.modB': sub-package B module (incl. sub-package A module A1)
>>> pyyc.__version__
'0.6'
>>> help(pyyc)
Help on package pyyc:
NAME
pyyc - Documentation du package pyyc.
PACKAGE CONTENTS
mod
subpkgA (package)
subpkgB (package)
VERSION
0.6
FILE
.../pyYC/pyyc/__init__.py
La structure du package est alors la suivante::
pyyc # package principal
pyyc.mod # top-level module
pyyc.subpkgA # sous-package A
pyyc.subpkgA.modA1
pyyc.subpkgA.modA2
pyyc.subpkgB # sous-package B
pyyc.subpkgB.modB
pyyc.subpkgA.modA1 # ce module a été importé par modB
.. index::
import relatif
pair: __all__; import *
Notez dans ce contexte:
* l'utilisation des imports *relatifs* (:pep:`328`), p.ex.:
.. code-block:: python
# Depuis pyyc/subpkgB, l'accès des différents symboles se fait par...
from . import modB # → modB.version
from .modB import version # → version (celui de modB)
from .. import subpkgA # → subpkgA.modA1.version
from ..subpkgA import modA1 # → modA1.version (celui de subpkgA)
from ..subpkgA.modA1 import version # → version (celui de subpkgA.modA1)
from ...pkg import __version__ # → __version__ (celui de pkg)
* l'utilisation de la variable :obj:`__all__ `, qui
permet d'expliciter les symboles importés par `import *`.
.. Attention:: Le `import *` doit être réservé aux seuls fichiers `__init__.py`!
Scripts
-------
.. index:: pair: packaging; __main__.py
Il est possible d'associer un (ou plusieurs) scripts à votre package, afin
qu'il ne soit pas utilisé uniquement comme une bibliothèque. Si un de vos
(sous-)packages contient un fichier :obj:`__main__.py `, il
sera directement exécuté comme un script par la commande `python -m`, p.ex.::
$ python -m pyyc # exécution du fichier `pyyc/__main__.py`
$ python -m pyyc.subpkgA # exécution du fichier `pyyc/subpkgA/__main__.py`
Il est toutefois préférable de configurer des `Points d'entrée`_ à
votre package, directement utilisable depuis la ligne de commande.
Configuration
=============
.. index:: pair: packaging; pyproject.toml
Le fichier `pyproject.toml
`_
est un fichier (au format texte :wfr:`TOML`) de configuration
générique utilisé non seulement par les différents outils de gestion
de package (:pypi:`setuptools`, :pypi:`build`, :pypi:`pip`, etc.),
mais aussi par d'autres outils externes tels que les vérificateurs de
code, les testeurs, etc.
* La section `[build-system]` est optionnelle mais fortement recommandée, elle
déclare l'outil de packaging à utiliser.
* La section `[project]`, obligatoire, déclare les méta-données de base du
projet, telles que le nom des auteurs, la version du projet, la version
minimale de python à utiliser, les dépendances, etc.
* La section `[tool]` configure spécifiquement chaque outil,
p.ex. `[tool.setuptools]` ou `[tool.pytest]`.
Le fichier `pyproject.toml`, à inclure dans le répertoire racine du
*package*, a typiquement la forme suivante:
.. literalinclude:: pyproject.toml
:language: toml
Ainsi configuré, votre *package* pourra être manipulé par les
différents outils de packaging, selon les objectifs,
p.ex. :pypi:`build` pour la construction d'une archive source du
*package*, :pypi:`pip` pour une installation, :pypi:`twine` pour une
publication sur `PyPI `_, etc.
Point d'entrée
--------------
.. index:: pair: packaging; point d'entrée
Un point d'entrée est une *fonction* du package qui sera
automatiquement encapsulée dans un script pour être directement
éxécutable depuis la ligne de commande, p.ex.:
* le fichier `mypackage/__main__.py` contient:
.. code-block:: python
def main(): # Sera utilisé comme point d'entrée
print("Programme principal")
if __name__ == "__main__": # Sera utilisé par `python -m mypackage`
main()
* le fichier `pyproject.toml` contient:
.. code-block:: toml
[project.scripts]
# le script myscript correspond à la fonction main() du fichier mypackage/__main__.py
myscript = "mypackage.__main__:main"
Le script `myscript` est alors automatiquement généré et installé lors de
l'installation du package, et disponible sur la ligne de commande de façon
autonome::
$ myscript
Programme principal
**Voir également:** :pypi:`pyyc` pour une illustration pratique.
Installation locale
===================
.. index:: pair: module; pip
L'installation locale du *package* se fait depuis son répertoire
racine avec l'outil :pypi:`pip`::
$ python -m pip install .
va installer le *package* de façon « statique »: le code installé est
une copie figée du code de développement à l'instant de
l'installation.
Il est également possible d'installer le *package* en mode
« dynamique » (dit « éditable »)::
$ python -m pip install -e .
Le code installé est alors un lien direct vers le code de
développement, et se met à jour automatiquement sans nécessité de le
réinstaller à chaque modification mineure.
.. rubric:: Exercice:
:ref:`Exercices/exo:packaging` (1e partie)
Distribution
============
Code source
-----------
Il est possible de générer une archive comprenant l'ensemble du code source
nécessaire à l'installation depuis le répertoire racine::
$ python -m build # génère le fichier dist/pyyc-1.0.tar.gz
Quiconque a ce fichier peut installer à partir du code source::
$ tar xvzf pyyc-1.0.tar.gz # Crée le répertoire pyyc-1.0/ ...
$ cd pyyc-1.0 # ... dans lequel se trouve les sources...
$ python -m pip install . # ... prêtes à être installées.
Dépôt git
---------
Pour un projet à distribution restreinte (p.ex. uniquement auprès de quelques
collaborateurs), le plus simple est de le distribuer en donnant accès (public
ou semi-privé) au dépôt de code en ligne (p.ex. `github
`_, `gitlab.in2p3 `_, etc.).
Il est alors possible d'installer ce package soit directement depuis le dépôt en
ligne::
$ python -m pip install git+https://gitlab.in2p3.fr/ycopin/pyyc.git
soit en clonant explicitement le dépôt et en réalisant une installation locale
(nécessaire si l'on veut pouvoir modifier localement le code)::
$ git clone https://gitlab.in2p3.fr/ycopin/pyyc # clonage du repo
$ cd pyyc
$ python -m pip install . # installation locale
Dépôt PyPI
----------
.. index::
pypi
pair: module; build
pair: module; twine
Pour un projet à plus grande audience, il est intéressant de le publier sur
PyPI_ (*paï-pi-aïe*), le dépôt tiers officiel des librairies externes et libres
de Python, à l'aide des outils :pypi:`build` et :pypi:`twine` (outils externes
de la `pyPA `_). Dans les cas simples (p.ex. projet pur
Python, et sous réserve de certaines dépendances...)::
$ python -m build # Construction des archives sources
$ python -m twine check dist/* # Vérification des archives
Checking dist/pyyc-1.0-py3-none-any.whl: PASSED
Checking dist/pyyc-1.0.tar.gz: PASSED
$ python -m twine upload --repository testpypi dist/* # Dépôt sur testpypi
Et voilà!
.. Note:: Avant de déposer votre projet sur le site PyPI_, testez votre
procédure sur le site de test `TestPyPI
`_ à l'aide de l'option
`--repository testpypi`. Dans les deux cas, il vous faudra créer un compte.
Vous pouvez maintenant vérifier l'existence de votre projet
(p.ex. :pypi:`pyyc`), et l'installer directement::
$ pip install pyyc
.. rubric:: Voir également:
* `Modules & Packages
`_;
* `An Overview of Packaging for Python
`_
* `Packaging projects
`_;
* `Packaging and distributing projects
`_;
* :rtfd:`Hitchhiker’s Guide to Packaging `;
* `Welcome to Python Packages! `_;
* `Python Modules and Packages – An Introduction
`_, `How to Publish
an Open-Source Python Package to PyPI
`_;
* `Python Packages `_.
.. rubric:: Notes de bas de page
.. [#pythonpath] Sous réserve que le chemin du répertoire (outre le répertoire
courant) soit connu de python:
>>> import sys
>>> sys.path
.. |fr| image:: ../_static/france_flag_icon.png
:alt: Fr
.. |en| image:: ../_static/uk_flag_icon.png
:alt: En