.. 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