class: center, middle background-image: url(img/mire.jpg) .footnote.preintro[La connextion au projecteur semble bonne. On peut commencer.] --- name: inverse layout: true class: inverse --- class: center, middle #"Packager" un projet Django .footnote[par [Stéphane "Twidi" Angel](http://twidi.com)] --- class: center # Pourquoi packager .left[
##• Versions
##• Partage
##• Déploiement ] --- class: center # Comment versionner .left[
##• cp -a projet "projet-\`date +%F\`"
##• git tag \`date +%F\` ] --- class: center # Comment .important.bold[NE PAS] versionner .left[
##• cp -a projet "projet-\`date +%F\`"
##• git tag \`date +%F\` .important[*] .footnote[.important[*]Bon, à la rigueur...] ] --- class: center # Comment partager .left[
##• partage réseau
##• copie sur clé USB
##• transfert FTP
##• transfert SCP ] --- class: center # Comment .important.bold[NE PAS] partager .left[
##• partage réseau
##• copie sur clé USB
##• transfert FTP
##• transfert SCP ] --- class: center # Comment déployer .left[
##• coder en prod
##• transfert FTP
##• transfert SCP
##• git pull ] --- class: center # Comment .important.bold[NE PAS] déployer .left[
##• coder en prod
##• transfert FTP
##• transfert SCP
##• git pull ] --- class: center, middle # Solution: le .important.bold[PACKAGING] --- class: center # Le packaging: .left[
## • apporte la gestion des dépendances
## • apporte la gestion des versions
## • permet de savoir où trouver le package
## • rend la publication simple et rapide ] --- class: center # Structure d'un projet (**Exemple** de structure d'un simple projet Django) .left.no-hl.no-border[ ``` ── py_foo -> répertoire principal (dépôt git...) * ├── foo -> le projet à packager * │ ├── apps -> apps django propre au projet * │ │ └── (...) * │ ├── locale -> quelques inévitables dossiers... * │ │ └── (...) * │ ├── media * │ │ └── (...) * │ ├── static * │ │ └── (...) * │ ├── templates * │ │ └── (...) * │ ├── __init__.py -> ... et fichiers * │ ├── urls.py * │ └── wsgi.py ├── README.md -> pour dire de quoi le projet parle ├── requirements.txt -> les dépendances de votre projet └── setup.py -> fichier qui permet de faire le package ``` .footnote[Je n'ai pas mis .bold[manage.py] car vous préférez bien sûr utiliser .bold[django-admin]] ] --- .center[ # Le fichier .important[setup.py] ## Sa structure de base ]
```python from setuptools import find_packages, setup setup( name='foo', version='0.0.1', packages=find_packages(), ) ``` --- .center[ # Le fichier .important[setup.py] ## En général un peu plus toufu ]
```python from setuptools import find_packages, setup setup( name='foo', version='0.0.1', packages=find_packages(), include_package_data=True, description='Foo, Bar Baz!', long_description='Foo, a deer, a female deer... ' 'Bar a note to follow Foo... ' 'Baz, a name, I call myself...', url='https://github.com/myself/foo/', author='My Self', author_email='foo@bar.com', # * ) ``` .footnote[\* Rappelez-vous d'aller un jour voir http://bar.com. Non, pas maintenant, merci ;)] --- class: center # Le fichier .important[setup.py]
## Création automatique
.bigger[ ```bash $ python createsetup --name=foo --version=0.0.1 ``` ] -- .center[ # .important.bold[MOUAH AH AH AH AH] ] --- class: center # Le fichier .important[setup.py]
## .important[PAS DE] création automatique
.strike-code.bigger[ ```bash $ python createsetup --name=foo --version=0.0.1 ``` ] --- class: center # Créer un fichier .important[setup.py]
.left[ ### 1. trouver sur le web un fichier setup.py ### 2. copier coller son contenu ### 3. faire les modifications nécessaires ### 4. publier le package ### 5. modifier ce qui ne l'a pas été à l'étape 3 ### 6. republier le package ] --- class: center # Le fichier .important[setup.py] en détail .left[ ###Nous allons voir: ### • version ### • description ### • dépendances ### • fichiers non-python (assets, templates, data...) ] --- class: center # Version ## Pep 440 .left.no-hl.no-border[ ```text 1.0.dev456 1.0a1 1.0a2.dev456 1.0a12.dev456 1.0a12 1.0b1.dev456 1.0b2 1.0b2.post345.dev456 1.0b2.post345 1.0rc1.dev456 1.0rc1 1.0 1.0+abc.5 1.0+abc.7 1.0+5 1.0.post456.dev34 1.0.post456 1.0.1 1.1.dev1 ``` ] --- class: center # Version: hard-coding .left[ ### py\_foo/foo/\__\_init_\__.py ```python #... __version__ = '0.0.1a1' VERSION = tuple(__version__.split('.')) # VERSION est maintenant (0, 0, '1a1') #... ``` ### py_foo/setup.py ```python #... version='0.0.1a1', #... ``` ] -- .center[ # .important.bold[NOT DRY] .footnote[.important[D]on't .important[R]epeat .important[Y]ourself] ] --- class: center # Version: importing .left[ ### py\_foo/foo/\__\_init_\__.py ```python #... __version__ = '0.0.1a1' VERSION = tuple(__version__.split('.')) #... ``` ### py_foo/setup.py ```python from foo import __version__ #... version=__version__, #... ``` ] -- .center[ ### Exécutera \__\_init_\__.py avant l'installation des dépendances, ce qui peut être problématique. ] --- class: center # Version: parsing .left[ ```python import ast, codecs, os class VersionFinder(ast.NodeVisitor): def __init__(self): self.version = None def visit_Assign(self, node): if node.targets[0].id == '__version__': self.version = node.value.s def read(*path_parts): filename = os.path.join(os.path.dirname(__file__), *path_parts) with codecs.open(filename, encoding='utf-8') as fp: return fp.read() def find_version(*path_parts): finder = VersionFinder() finder.visit(ast.parse(read(*path_parts))) return finder.version #... * version=find_version('foo', '__init__.py'), ``` .footnote[Emprunté à [django-compressor](https://github.com/django-compressor/django-compressor/blob/develop/setup.py)] ] --- class: center # Description
.left[ ### • .important[description] Nom qui apparaîtra par exemple dans les résultats de *pip search* ### • .important[long_description] Texte qui apparaîtra par exemple sur les pages web de PyPI. ] --- class: center # Description: hard-coding .left[ ### py\_foo/foo/\__\_init_\__.py ```python """Foo, Bar Baz!""" # docstring #... ``` ###py_foo/README.md ``` Foo, a deer, a female deer... Bar a note to follow Foo... Baz, a name, I call myself... ``` ### py_foo/setup.py ```python #... description="Foo, Bar Baz!", long_description='Foo, a deer, a female deer... ' 'Bar a note to follow Foo... ' 'Baz, a name, I call myself...', #... ``` ] --- class: center # Description: hard-coding ## .important.bold[Toujours pas DRY] --
# Description: importing ## .important.bold[Toujours pentiellement problématique] --- class: center # Description: parsing Récupérer la description depuis le docstring du fichier py\_foo/foo/\__\_init_\__.py .left[ .strike-code[ ```python def find_version(*path_parts): finder = VersionFinder() finder.visit(ast.parse(read(*path_parts))) return finder.version ``` ] .noop[ ```python def find_infos(*path_parts): finder = VersionFinder() node = ast.parse(read(*path_parts)) * docstring = ast.get_docstring(node) finder.visit(node) return finder.version, docstring *version, descrption = find_infos('foo', '__init__.py'), #... setup( version=version, description=description, #... ) ``` ] ] --- class: center # Description: parsing Récupérer la longue description depuis le contenu du fichier py_foo/README.md .left[ ```python #... long_description=read('README.md'), #... ``` ] .footnote[Ou plutôt *.rst* si vous voulez une mise en page correcte sur PyPI] --- class: center # Évolution de notre setup .left[
```python #... package_name = 'foo' version, descrption = find_infos(package_name, '__init__.py'), setup( name=package_name, version=version, packages=find_packages(), include_package_data=True, description=description, long_description=read('README.md'), url='https://github.com/myself/foo/', author='My Self', author_email='foo@bar.com' ) ``` .footnote[.important[url], .important[author] et .important[author_email] peuvent aussi être gérés de la même façon.] ] --- class: center # Dépendances
-- .left.no-hl[ Exemple de requirements.txt: ``` django==1.8.1 psycopg2==2.6 django-extended-choices==1.0.4 ``` ] --- class: center # Dépendances
## Via _pip install_
.left.bigger[ ```bash $ pip install -r requirements.txt ``` ] .footnote[Pas de sudo car dans un virtualenv] -- .left[ ## .important[OK, mais pour développer] ] --- class: center # Dépendances
## Via le fichier de setup
.left[ ```python setup( #... install_requires=[ 'django==1.8.1', 'psycopg2==2.6', 'django-extended-choices==1.0.4', ], #... ) ``` ] -- .center[ # .important.bold[Toujours pas DRY] ] --- class: center # Dépendances
.left[ ```python from pip.req import parse_requirements def get_requirements(source): install_reqs = parse_requirements(source) return set([str(ir.req) for ir in install_reqs]) setup( #... install_requires=get_requirements('requirements.txt'), #... ) ``` ] --- class: center # Gestion des sessions Pip (1.5+)
.left[ ```python from pip.req import parse_requirements # Code # The `session` argument for the `parse_requirements` function is available (but # optional) in pip 1.5, and mandatory in next versions try: from pip.download import PipSession except ImportError: parse_args = {} else: parse_args = {'session': PipSession()} def get_requirements(source): install_reqs = parse_requirements(source, **parse_args) return set([str(ir.req) for ir in install_reqs]) setup( #... install_requires=get_requirements('requirements.txt'), #... ) ``` ] --- class: center # Assets
.left[ ### • templates ### • CSS ### • JS ### • data (JSON...) ] --
.center[ ### \+ tout ce qui n'est pas python ] --- class: center # Assets
.center[ ## Fichier .important[MANIFEST.in]
] -- .left.no-hl[ ``` include README.md include MANIFEST.in include requirements.txt recursive-include foo *.html recursive-include foo/static * recursive-exclude foo/media * recursive-include foo/locale *.mo recursive-exclude * __pycache__ recursive-exclude * *.py[co] ``` ] --- class: center # Générations d'assets --
## Kesako ? --
.left[ ### • sass -> css ### • coffee -> js ### • gulp, grunt... ] --- class: center # Générations d'assets
## Deux possibilités:
### 1. avant de lancer le setup --
.center[ ### .important[Problème: et si on oublie ?] ] --- class: center # Générations d'assets
## Deux possibilités:
### 2. pendant le setup .center[ ### .important[Garantit que le paquet contient tout ce qu'il y avait au moment précis de sa génération] ] --- class: center # Générations d'assets
.left[ ```python from distutils.command.sdist import sdist from subprocess import check_call class foo_sdist(sdist): def run(self): if not self.dry_run: check_call(['invoke', 'prepare-build']) # exemple # distutils uses old-style classes, so no super() sdist.run(self) setup( #... cmdclass={'sdist': foo_sdist}, #... ) ``` ] --- class: center # Fichier setup final, partie 1/4
.left.smaller[ ```python import ast import codecs import os from distutils.command.sdist import sdist from pip.req import parse_requirements from setuptools import find_packages, setup from subprocess import check_call # Configuration package_name = 'foo' long_doc_file = 'README.md' classifiers = [ "Development Status :: 4 - Beta", "Operating System :: OS Independent", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", ] # /Configuration (don't touch below) ``` ] --- class: center # Fichier setup final, partie 2/4
.left.smaller[ ```python # Code # The `session` argument for the `parse_requirements` function is available (but # optional) in pip 1.5, and mandatory in next versions try: from pip.download import PipSession except ImportError: parse_args = {} else: parse_args = {'session': PipSession()} def get_requirements(source): install_reqs = parse_requirements(source, **parse_args) return set([str(ir.req) for ir in install_reqs]) class VersionFinder(ast.NodeVisitor): def __init__(self): self.data = {} def visit_Assign(self, node): if node.targets[0].id in ( '__version__', '__author__', '__contact__', '__homepage__', '__license__', ): self.data[node.targets[0].id[2:-2]] = node.value.s ``` ] --- class: center # Fichier setup final, partie 3/4
.left.smaller[ ```python def read(*path_parts): filename = os.path.join(os.path.dirname(__file__), *path_parts) with codecs.open(filename, encoding='utf-8') as fp: return fp.read() def find_info(*path_parts): finder = VersionFinder() node = ast.parse(read(*path_parts)) finder.visit(node) info = finder.data info['docstring'] = ast.get_docstring(node) return info package_info = find_infos(package_name, '__init__.py'), ``` ] --- class: center # Fichier setup final, partie 4/4
.left[ ```python setup( name=package_name, version=package_info['version'], packages=find_packages(), include_package_data=True, description=package_info['docstring'], long_description=read(long_doc_file), url=package_info['homepage'], author=package_info['author'], author_email=package_info['contact'], install_requires=get_requirements('requirements.txt'), license=package_info['license'], classifiers=classifiers, ) ``` ] --- class: center # Notre fichier \__\_init_\__.py
.left[ ```python """Foo, Bar Baz!""" __author__ = 'My Self' __contact__ = 'foo@bar.com' __homepage__ = 'https://github.com/myself/foo/' __license__ = 'BSD' __version__ = '0.0.1' VERSION = tuple(__version__.split('.')) ``` ] --- class: center # Upload
.left.bigger[ - sur PyPI si paquet public - ou sur un dépôt privé (devpi, gemfury...)
```bash $ python setup.py sdist upload ```
Si vous voulez (pouvez) en faire un "wheel": ```basj $ python setup.py sdist bdist_wheel upload ``` ] .footnote[C'est une bonne idée de tester le paquet avant l'upload, en fait] --- class: center # Install
.left.bigger[ ```bash $ pip install foo ``` ] .center[ou] .left.bigger[ ```bash $ pip install foo==0.0.1a1 ``` ]
.bigger.left[ Installera: - le code du projet - les dépendances - les assets pré-"compilés" ] --- class: inverse, center, middle # MERCI ! .footnote[Retrouvez cette présentation sur http://twidi.github.io/] --- class: center, middle layout: true background-image: url(img/mire.jpg) .footnote.preintro[Signal lost...] ---