Autour du codeDevelopper toujours mieux
Posté le

Distribuer un paquet python via pypi

Nous allons voir comment distribuer un projet python sur pypi pour que n'importe qui puisse l'installer en utilisant la commande pip install.

Pypi, c'est quoi ?

Pypi signifie Python Package Index. C'est un site dont le but est de recenser et de mettre à disposition de la communauté des paquets Python. Il est accecible à l'addresse pypi.python.org

Organiser son projet pour le distribuer

Dans cet exemple, on va distribuer un projet fictif nommé felkafe. Voici comment celui-ci est organisé.

.
├── docs
├── felkafe
│   ├── __init__.py
│   ├── __main__.py
│   ├── module.py
│   └── other_module.py
├── LICENSE.txt
├── README.rst
├── setup.py
└── tests
    ├── __init__.py
    ├── module.py
    └── other_module.py

Le répertoire felkafe est votre packet python. Ici on utilise un packet, mais rien ne vous empêche d'avoir un seul module. C'est juste une question d'organisation du code. Si votre module est trop gros (plus de 1000 lignes pour pylint) il est préférable de le décomposer.

Les deux organisations ci-dessous sont complètement équivalentes

Dans un seul module:

PY = 3.14

if __name__ == '__main__':
    print('PY: {}'.format(PY))

Dans un package:

PY = 3.14

from . import PY
print('PY: {}'.format(PY))

Dans les 2 cas vous pourrez écrire from felkafe import PY dans d'autre projet python, ou exécuter votre module via la commande python -m felkafe pour afficher PY: 3.14

Le répertoire docs contient la documentation.

Le répertoire tests contient les tests unitaires de votre projet, ce répertoire n'a pas à être distribué. Les tests doivent être exécutés par le développeur du paquet pas par l'utilisateur du paquet

Les fichiers suivants sont obligatoires:

LICENSE.txt contient les détails de la licence que vous avez choisie pour distribuer votre logiciel. Sans ce fichier dans votre paquet, le droit d'auteur s'applique et la redistribution devient illégale. Pypi ne distribue donc pas de package sans licence. Si vous ne savez pas quelle licence choisir, voici un tableau des licences les plus connues.

README.rst Ce fichier peut être écrit en reStructuredText sinon un README.txt fera l'affaire. L'important, c'est qu'il contienne le nom de votre package, ce qu'il fait, comment l'installer, un petit exemple et un lien vers la doc.

Le fichier setup.py

Le fichier setup.py est le point d'entrée du programme chargé de transformer votre paquet en distribuable.

from setuptools import setup

setup(
    name='felkafe',
    version='1.0.1',
    description='distribution example',
    keywords='distribution pypi setup.py',
    author='felkafe teams',
    author_email='teams@felkafe.com',
    url='https://github.com/felkafe/felkafe',
    license='GPLv3',
    packages=['felkafe'],
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Developers',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Topic :: Software Development :: Testing',
        'License :: OSI Approved :: GNU General Public License v3 (GPLv3)'
    ],
    python_requires='>=3.4',
    install_requires=['voluptuous']
)

On va décrire quelques-uns de ces paramètres:

version

Ce champ doit contenir la version de votre paquet et son format doit respecter la PEP 440. Pour simplifier grandement la vingtaine de pages de la PEP 440. Sachez que 2 ou 3 nombres séparés par des points est un format de version valide.

Si vous souhaitez importer la valeur de ce champ directement de votre paquet, sachez que cela est très risqué.

Generalement on met une variable __version__ dans le __init__.py qui contient la version du paquet. Cette variable est une convention pris en exemple dans la PEP 8.

Si par exemple le module 'felkafe.module' contient la classe Bidule, et que vous souhaitez que Bidule soit directement importable de felkafe. Votre __init__.py va resembler à ceci:

from .module import Bidule

__version__ = '1.0.1'

__all__ = ['__version__', 'Bidule']

import voluptuous

class Bidule:
    ...

Le problème c'est que lors de l'installation de votre paquet le setup.py va être lu et il va importer felkafe.__version__, mais votre __init__.py en important Bidule va devoir importer tous les modules tiers utilisés dans felkafe.module. En gros le setup.py ne peut pas fonctionner si l'utilisateur n'a pas déjà installé les modules qui devraient être automatiquement installés.

Une solution et de faire un module felkafe.__version__.py et dans le setup.py vous importez __version__.py en fessant comme si felkafe était un répertoire normal et non un paquet python.

from setuptools import find_packages, setup
import sys

sys.path.insert(0, './felkafe')
from __version__ import __version__

setup(
    name='felkafe',
    version=__version__,
    ...)

__version__ = '1.0.1'

from .__version__ import __version__
from .module import Bidule

__all__ = ['__version__', 'Bidule']

keywords

Une liste de mots décrivant le projet séparé par des espaces.

packages

La liste contenant les packets à distribuer. Si vous écrivez cette liste à la main, il faut garder à l'esprit que les sous-paquets ne sont pas inclus. Cela signifie que si votre application évolue comme ceci:

.
├── felkafe
│   ├── __init__.py
│   ├── module.py
│   ├── other_module.py
│   └── subpackage
│       ├── __init__.py
│       └── submodule.py
├── LICENSE.txt
├── README.rst
└── setup.py

Le champs packages du setup.py devra contenir ['felkafe', 'felkafe.subpackage'] pour ne pas distribuer un paquet incomplet. Vous pouvez si vous le souhaitez automatiser la recherche des packet en utilisant setuptools.find_packages. La fonction find_packages va lister tous les packages et sub-package qu'elle trouve. Si vous avez par exemple un package test à la racine de votre projet, il va donc être listé aussi. Pour éviter ça, vous pouvez utiliser le paramètre exclude.

Il faudra donc penser à le mettre à jour à chaque fois que vous ajouter un package dans votre projet qui n'est pas destiné à être distribué telle que des plugins perso pour votre fabfile ou des tests en plus. Pour ne plus avoir à mettre à jour le champ package, une possibilité est d'utiliser le patron de code suivant:

packages=['felkafe'] + [
    'felkafe.{}'.format(subpackage)
    for subpackage
    in find_packages(where='./felkafe')
]

Avec ce patron, vous n'aurais plus à modifier votre setup.py sauf si vous souhaitez distribuer plusieurs paquets distincts au sein d'un même projet.

classifiers

Les classifiers permettent de trier les projets sur Pypi. Voici la liste des classifiers que vous pouvez utiliser.

python_requires

Indique quelle version de python est requise pour utiliser votre paquet. C'est le numéro de la version précédé par l'une des clause suivante.

ClauseSignification
==X.Y.ZLa version doit être X.Y.Z
>=X.Y.ZLa version doit être la X.Y.Z ou une version supérieure
>X.Y.ZLa version doit être une version supérieure à la X.Y.Z
!=X.Y.ZLa version ne doit pas être la version X.Y.Z
<=X.Y.ZLa version doit être inférieure ou être la version X.Y.Z
<X.Y.ZLa version doit être inférieure à la version X.Y.Z

Vous pouvez utiliser des étoiles comme jocker:

!=3.* aucune version 3 n'est supporté

Et vous pouvez mettre plusieurs clauses séparées par des virgules

La version de python 2.7 ou 3.5 et supérieure:

'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4'

install_requires

Ce champ est utilisé par pip pour installer les autres paquets que votre paquet à besoin pour fonctionner. Il doit contenir la liste minimale des paquets à installer pour que votre paquet puisse fonctionner. C'est une liste qui prend en paramètre des chaînes de caractère au même format que les clauses dans python_requires.

Exemple d'un paquet qui besoin de voluptuous dans sa version 1.10 ou supérieure et de attrs dans une version entre 17 et 20:

install_requires=[
    'voluptuous>=1.10',
    'attrs>=17, attrs<20'
]

Les prérequis ne devraient pas être bornés à des version uniques, mais au contraire être le plus large possible.

On dit que c'est est une liste de prérequis abstrait dans le sens où on ne spécifie pas précisément une version ou un lieu de téléchargement. Ce sera le rôle de la commande pip qui précisera l'index où récupérer le paquet et la version précise du paquet.

Par défaut pip récupère la dernière version stable trouvée sur pypi. Si vous avez un fichier requirements.txt, évitez de dupliquer automatiquement le contenu de ce fichier dans le install_requires. Le requirements.txt est simplement une liste de paramètres pour pip. Ainsi requirements.txt est destiné à définir les prérequis pour un environnement Python complet. Vous allez pouvoir fixer des versions précises pour rendre une installation répétable. Votre requirements.txt pourra par exemple contenir un prérequis vers un framework de test qu'utilisera la CI pour lancer les tests. Cela n'a pas lieu d'être dans le install_requires, car vous ne distribuez pas les tests avec votre paquet.

Transformer son paquet en distribuable

Premierement on va s'assurer d'avoir la dernière version de pip, setuptools et twine.

$ pip install -U pip twine setuptools

On peut distribuer sous forme de source via la commande suivante.

$ python setup.py sdist

Ou sous forme de Wheel via la commande:

$ python setup.py bdist_wheel

Uploader son paquet

Pypi possède une zone de test où les projets déposés son supprimer toutes les 24 heures. On va utiliser cette zone de teste pour ne pas polluer le pypi publique avec des librairies inutiles.

Premierement, enregistrer son projet sur le pypi de test

Un lien de confirmation vous sera envoyé par email.

On va ensuite uploader le packet via la command suivante:

twine upload --repository-url https://test.pypi.org/legacy/ dist/*

Si vous souhaitez ne plus avoir à saisir votre nom d'utilisateur et votre mot de passe à chaque fois, vous pouvez créer un fichier .pypirc de configuration dans votre home

[distutils]
index-servers =
    pypi
    pypitest

[pypi]
repository:https://pypi.python.org/pypi
username:votre-nom
password:votre-mot-de-pass

[pypitest]
repository:https://testpypi.python.org/pypi
username:votre-nom
password:votre-mot-de-pass

Avec ce fichier pour publier sur le server de test:
twine upload --repository pypitest
Avec ce fichier pour publier sur le server standard:
twine upload --repository pypi

Tester l'installation de son package

Si vous avez envoyé votre paquet sur le pypitest vous pouvez le récupérer comme ceci:

pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple felkafe

Le premier paramètre --index-url indique ou allez chercher le paquet, le paramètre --extra-index-url permet à pip d'aller chercher sur le dépôt officiel les paquets qu'il ne trouverai pas sur le dépot de test. C'est utile si votre paquêt à besoin d'installer des dépendances.

Allez à vos IDE et bon code! smile