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 par import *.

    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:

Notes de bas de page