Python packaging

Tour d'horizon en 15 minutes

Packaging Python

Par Axel Haustant

coverlogo meetup

Packaging ?

Pourquoi ?

Être réutilisable

Comment ?

La solution: setuptools !

Les bases

setup.py

from setuptools import setup

setup(
    name='my-project',
    version='0.1.0',
    # ...
)

C'est du Python donc tout est permis !

Identification

Versionning

Gestion des dépendances

extras_require = {
    'tests':  ['factory-boy']
}
$ pip install my-project[tests]

Gestion des resources

README

Doit permettre de démarrer rapidement.

Commandes

Développez

Pour être prêt à développer:

$ python setup.py develop
# ou
$ pip install -e .

A refaire dès que les dépendances et les entrypoints changent.

Prévisualisez

Contrôlez ce que vous allez publier

$ python setup.py --long-description | rst2html
$ python setup.py sdist

Publiez

# Enregistrer le module sur PyPI
$ python setup.py register
# Publier sur PyPI
$ python setup.py sdist upload
# Créer un version avec un suffix
$ python setup.py -q egg_info -b ".1234" sdist

Réutilisez !

Réutiliser les metadonnées du module

Selon la PEP 396, le module doit contenir un attribut __version__

from project import __version__, __description__
setup(
    name='project'
    version=__version__
    description=__description__
)

Réutiliser les requirements de pip

RE_REQUIREMENT = re.compile(r'^\s*-r\s*(?P<filename>.*)$')

def pip(filename):
    requirements = []
    for line in open(join('requirements', filename)).readlines():
        match = RE_REQUIREMENT.match(line)
        if match:
            requirements.extend(pip(match.group('filename')))
        else:
            requirements.append(line)
    return requirements

setup(
    # ...
    install_requires=pip('install.pip'),
    tests_require=pip('test.pip'),
    extras_require = {
        'tests':  pip('test.pip'),
    },
)

Réutiliser les fichiers rst

PYPI_RST_FILTERS = (
    (r'\.\.\s? code-block::\s*(\w|\+)+',  '::'), #
    (r'.*travis-ci\.org/.*', ''),
    (r'.*pypip\.in/.*', ''),
    (r'.*crate\.io/.*', ''),
    (r'.*coveralls\.io/.*', ''),
)

def rst(filename):
    content = open(filename).read()
    for regex, replacement in PYPI_RST_FILTERS:
        content = re.sub(regex, replacement, content)
    return content

long_description = '\n'.join((
    rst('README.rst'),
    rst('CHANGELOG.rst'),
    ''
))

Réutilisez la version

try:
    from pkg_resources import get_distribution
    VERSION = get_distribution('project').version
except:
    VERSION = __import__('project').__version__

Prend en compte la version "installée" (ex: 0.1.0.dev1234)

Entry Points

Console scripts

Pas besoin de répertoire bin

entry_points={
    'console_scripts': [
        'myexec = project.commands:main',
    ]
}
$ myexec

Créer ses propres commandes

entry_points = {
    'distutils.commands': 'do_it = project.commands:DoSomething',
},
from setuptools import Command

class DoSomething(Command):
    description = "Do something"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        do_something()

Chargement d'extensions

Un project qui exporte

entry_points = {
    'myproject.plugins': [
        'someplugin = other_project.plugins:SomePlugin',
    ],
},

Un autre qui importe

import pkg_resources

for entrypoint in pkg_resources.iter_entry_points('myproject.plugins'):
    plugin = entrypoint.load()

Un peu de lecture

Questions

A suivre...

Extras

Layout

├── doc
├── myproject
│   ├─ __init__.py
│   └─ ..
├── requirements
│   ├─ develop.pip
│   ├─ install.pip
│   ├─ tools.pip
│   └─ test.pip
├─ .gitignore
├─ Makefile/Fabfile
├─ bumpr.rc
├─ CHANGELOG.rst
├─ pep8.rc
├─ pylint.rc
├─ MANIFEST.in
├─ README.rst
└─ setup.py