2. Packaging🔗
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
setuptools et pyproject.toml, mais qui peut être amenée à
évoluer.
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:
- Modules
Un module est constitué d'un seul fichier, facilement distribuable et installable.
- Packages
Un package regroupe un ou plusieurs modules dans une arborescence dédiée.
2.1. 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 [1]:
>>> 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'
2.2. Package avec initialisation🔗
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.
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
└─ ...
Avertissement
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
Notez dans ce contexte:
l'utilisation des imports relatifs (PEP 328), p.ex.:
# 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
__all__, qui permet d'expliciter les symboles importés parimport *.Attention
Le
import *doit être réservé aux seuls fichiers__init__.py!
2.2.1. Scripts🔗
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 __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.
2.3. Configuration🔗
Le fichier pyproject.toml est un fichier (au format texte TOML) de configuration générique utilisé non seulement par les différents outils de gestion de package (setuptools, build, 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:
[build-system] # Build backend (here setuptools)
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project] # Generic package metadata
name = "mypackage"
description = "My first package"
authors = [
{ name="Yannick Copin", email="y.copin@ipnl.in2p3.fr" },
]
# Static or Dynamic version?
# version = "0.0.1"
dynamic = ["version"] # see [tool.setuptools.dynamic] section below
# README (filename)
readme = "README"
# License (filename)
license = {file="LICENSE"}
# Python version requirement
requires-python = ">=3.8"
# Main dependency management (see optional dependencies below)
# Only *external* libraries (standard lib. is always included)
dependencies = [
"numpy",
"matplotlib",
"astropy",
]
classifiers = [ # Optional (see https://pypi.org/classifiers/)
# How mature is this project?
"Development Status :: 1 - Planning",
# Indicate who your project is intended for
"Intended Audience :: Education",
# License (should match "license" above)
"License :: OSI Approved :: BSD License",
# Specify the Python versions you support (see below)
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
# Keywords
"Topic :: Education",
]
[tool.setuptools.dynamic] # Dynamic definitions
version = {attr="mypackage.__version__"} # set version from package
[tool.setuptools.packages] # Package discovery
find = {} # Scan the project directory with the default parameters
[project.urls]
Repository = "https://gitlab.in2p3.fr/ycopin/mypackage"
Documentation = "https://ycopin.pages.in2p3.fr/mypackage"
Ainsi configuré, votre package pourra être manipulé par les différents outils de packaging, selon les objectifs, p.ex. build pour la construction d'une archive source du package, pip pour une installation, twine pour une publication sur PyPI, etc.
2.3.1. Points 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__.pycontient: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.tomlcontient:[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: pyyc pour une illustration pratique.
2.4. Installation locale🔗
L'installation locale du package se fait depuis son répertoire racine avec l'outil 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.
Exercice:
Packaging (1e partie)
2.5. Distribution🔗
2.5.1. 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.
2.5.2. 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
2.5.3. Dépôt PyPI🔗
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 build et 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. pyyc), et l'installer directement:
$ pip install pyyc
Voir également:
Python Modules and Packages – An Introduction, How to Publish an Open-Source Python Package to PyPI;
Notes de bas de page