2. Packaging🔗
Avertissement
le packaging, la distribution et l'installation des packages sous Python fait encore l'objet de nombreux 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 « récente » basée sur setuptools, mais qui peut être amenée à évoluer (cf. PEP 518).
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:
>>> import pkg.mod1
>>> import pkg.subpkg.mod
Les (sous-) packages n'étant pas initialisés, ils ne contienent 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
:
pyYC/ # Répertoire racine du package pyYC
│
├─ pyyc/ # Répertoire source (package principal)
│ │
│ ├─ __init__.py # Initialisation du package lors de 'import pkg'
│ │
│ │┌ "Documentation du package pyyc."
│ ││
│ ││ __version__ = "0.1" # à màj régulièrement
│ ││
│ ││ from . import mod # import du module mod
│ ││ from . import subpkgA # import du sous-package subpkgA
│ │└ from . import subpkgB # import du sous-package subpkgB
│ │
│ ├─ mod.py # Module top-level
│ │
│ │┌ "Documentation du module mod."
│ ││
│ ││ __all__ = ['version'] # limite la portée de "import *"
│ ││ version = "top-level module"
│ │└ print("Initialisation", version) # PAS DE PRINT dans un vrai module!
│ │
│ ├─ subpkgA/ # Sous-répertoire du sous-package subpkgA
│ │ │
│ │ ├─ __init__.py # Initialisation du sous-package ('import pkg.subpkgA')
│ │ │
│ │ │┌ "Documentation du sous-package subpkgA."
│ │ ││
│ │ ││ __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
│ │ │
│ │ │┌ "Documentation du module modA1."
│ │ ││
│ │ ││ version = 'A1'
│ │ │└ print("Initialisation ", version) # À ÉVITER dans un vrai module!
│ │ │
│ │ └─ modA2.py # module
│ │
│ │ ┌ version = 'A2'
│ │ └ print("Initialisation ", version)
│ │
│ └─ subpkgB/ # sous-répertoire du sous-package subpkgB
│ │
│ ├─ __init__.py # Initialisation du sous-package ('import pkg.subpkgB')
│ │
│ │┌ "Documentation du sous-package subpkgB."
│ ││
│ │└ from .modB import * # import local
│ │
│ └─ modB.py # module
│
│ ┌ from ..subpkgA import modA1 # import relatif
│ │
│ │ version = 'B' + modA1.version
│ └ print("Initialisation ", 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
├─ setup.cfg # Fichier de configuration
├─ setup.py # Fichier d'installation
└─ ...
>>> import pyyc
Initialisation top-level module
Initialisation A1
Initialisation A2
Initialisation BA1
>>> pyyc.__version__
'0.1'
>>> help(pyyc)
Help on package pyyc:
NAME
pyyc - Documentation du package pyyc.
PACKAGE CONTENTS
mod
subpkgA (package)
subpkgB (package)
VERSION
0.1
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 un point d'entrée à votre package, directement utilisable depuis la ligne de commande (voir ci-dessous).
2.3. Configuration🔗
Nous allons configurer le package pour une distribution à l'aide de l'outil d'installation setuptools (qui ne fait pas partie de la bibliothèque standard [1]).
Le fichier de configuration setup.cfg
à inclure dans le répertoire racine du
package a typiquement la forme suivante:
[metadata]
name = mypackage
# Utilise l'attribut mypackage.__version__ pour la version
version = attr: mypackage.__version__
description = My first package.
# Utilise le contenu du fichier README pour la description
long_description = file: README
long_description_content_type = text/x-rst
author = Yannick Copin
author_email = y.copin@ipnl.in2p3.fr
license = BSD 3-Clause "New" or "Revised" License
license_files = LICENSE
url = https://gitlab.in2p3.fr/ycopin/mypackage
classifiers =
# Voir 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.
Programming Language :: Python :: 3.6
# Keywords
Topic :: Education
[options]
zip_safe = False
include_package_data = True
packages = find:
python_requires = >=3.6
# Dépendances externes uniquement (la librairie std est tjs inclue)
install_requires =
numpy
matplotlib
astropy
Le fichier d'installation setup.py
est alors trivial:
from setuptools import setup
setup()
Ainsi configuré, l'outil setup.py
[2] propose de nombreuses
fonctionnalités:
$ python setup.py --help-commands
[...]
build build everything needed to install
clean clean up temporary files from 'build' command
install install everything from build directory
develop install package in 'development mode'
sdist create a source distribution (tarball, zip file, etc.)
[...]
Attention
le format de setup.cfg
est assez pointilleux:
n'ajoutez pas de commentaires en fin de ligne (mais uniquement en
début de ligne), respectez les retours à la ligne de l'exemple.
2.3.1. Points d'entrée🔗
Un point d'entrée est une fonction du package encapsulée dans un script pour être directement éxécutable depuis la ligne de commande, p.ex.:
le fichier
mypackage/__main__.py
contient:def main(): # Sera utilisé comme point d'entrée dans setup.cfg print("Programme principal") if __name__ == "__main__": # Sera utilisé par `python -m mypackage` main()
le fichier
setup.cfg
contient:[options.entry_points] console_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 appelable directement:
$ myscript
Programme principal
Voir également: pyyc pour une application pratique.
2.4. Installation locale🔗
Localement (i.e. dans l'hypothèse où vous disposez d'une copie du répertoire source), l'installation du package se fait alors depuis son répertoire racine:
$ python setup.py install
Si – comme la plupart d'entre nous – vous utilisez l'outil pip pour gérer l'installation des bibliothèques externes, vous pouvez directement exécuter:
$ python -m pip install .
pour installer le package de façon pérenne. Pour une installation en mode « éditable »:
$ python -m pip install -e .
Le code installé est alors un lien direct vers le code de développement, sans nécessité de le réinstaller à chaque modification mineure.
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 setup.py sdist # génère le fichier dist/pyyc-0.1.tar.gz
Quiconque a ce fichier peut installer à partir du code source:
$ tar xvzf pyyc-0.1.tar.gz # créer le répertoire pyyc-0.1/ ...
$ cd pyyc-0.1 # ... dans lequel se trouve les sources...
$ python setup.py 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 modifier le code):
$ git clone https://gitlab.in2p3.fr/ycopin/pyyc # clonage
$ 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 pyPA). Dans les cas simples (p.ex. projet pur Python, et sous réserve de certaines dépendances…):
$ python -m build # Construction des fichiers d'installation
$ twine check dist/* # Vérification
Checking dist/pyyc-0.1-py3-none-any.whl: PASSED
Checking dist/pyyc-0.1.tar.gz: PASSED
$ 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
.
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