Templates de projet avec cookiecutter


Beaucoup de frameworks viennent avec des templates de projets maintenant. Django vient avec django-admin startproject --template, tandis que crossbar vient avec crossbar init --template. L’idée d’avoir de templates pour ses projets n’a rien de nouveau, d’ailleurs la plupart des OS modernes ont au moins ça intégré pour des fichiers. Ainsi Ubuntu à un dossière “Modèles”, et tout ce qu’il y a dedans peut être dupliqué ailleurs via un clic droit dans Nautilus.

Néanmoins la meilleure solution à ce jour vient de Audrey Roy, qui est avant tout connue pour avoir co-écrit le livre Two scoops of Django, et qui a aussi pondu cookiecutter.

L’outil est très simple. D’abord un pip pour l’installer :

pip install cookiecutter

Notez que bien que le projet soit en Python, les templates peuvent être en n’importe quel langage, cookiecutter s’en branle. Lui, il va juste copier le contenu du template dans un dossier, et remplacer les variables notées {{}} dedans.

Pour commencer, on créé un dossier nommé {{cookiecutter.repo_name}} qui va contenir le template de son projet. Oui, le nom du dossier est {{cookiecutter.repo_name}}. En effet les noms de dossiers et fichiers sont aussi templatisables, et passés à la moulinette de jinja2.

Dans ce dossier, on peut mettre tous les fichiers qu’on veut, avec des noms normaux, ou avec des variables. Le contenu des fichiers peut, bien entendu, contenir aussi des variables. Les variables sont toujours de la forme {{cookiecutter.nom_de_variable}}.

Ensuite, à côté de notre dossier nommé {{cookiecutter.repo_name}} (le dossier du template), on peut créer un fichier cookiecutter.json qui va contenir les valeurs par défaut de nos variables :

{
    "repo_name": "nouveau_projet",
    "version": "0.1.0",
    "autre_variable": "Bidule"
}

Enfin pour récolter les fruits de son labeur, on lance cookiecutter /chemin/vers/template et l’outil va vous poser tout un tas de question pour remplir les variables, puis va générer le nouveau projet dans le dossier courant.

Cookiecutter accepte aussi des urls de repo git et mercurial comme source de template, et l’auteur de l’outil fournit un template de projet Python très complet par ce biais.

On va l’utiliser comme exemple.

Voici son contenu :

├── cookiecutter.json <= valeur par défaut des variables
├── {{cookiecutter.repo_name}} <= template du projet
│   ├── AUTHORS.rst
│   ├── CONTRIBUTING.rst
│   ├── {{cookiecutter.repo_name}}
│   │   ├── {{cookiecutter.repo_name}}.py
│   │   └── __init__.py
│   ├── docs
│   │   ├── authors.rst
│   │   ├── conf.py
│   │   ├── contributing.rst
│   │   ├── history.rst
│   │   ├── index.rst
│   │   ├── installation.rst
│   │   ├── make.bat
│   │   ├── Makefile
│   │   ├── readme.rst
│   │   └── usage.rst
│   ├── HISTORY.rst
│   ├── LICENSE
│   ├── Makefile
│   ├── MANIFEST.in
│   ├── README.rst
│   ├── requirements.txt
│   ├── setup.cfg
│   ├── setup.py
│   ├── tests
│   │   ├── __init__.py
│   │   └── test_{{cookiecutter.repo_name}}.py
│   └── tox.ini
└── README.rst

Certains usages de variables sont assez évident, comme par exemple le fichier __init__:

# -*- coding: utf-8 -*-
 
__author__ = '{{ cookiecutter.full_name }}'
__email__ = '{{ cookiecutter.email }}'
__version__ = '{{ cookiecutter.version }}'

D'autres sont assez malines et inattendues comme le fichier de test :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
"""
test_{{ cookiecutter.repo_name }}
----------------------------------
Tests for `{{ cookiecutter.repo_name }}` module.
"""
 
import unittest
 
from {{ cookiecutter.repo_name }} import {{ cookiecutter.repo_name }}
 
 
class Test{{ cookiecutter.repo_name|capitalize }}(unittest.TestCase):
 
    def setUp(self):
        pass
 
    def test_something(self):
        pass
 
    def tearDown(self):
        pass
 
if __name__ == '__main__':
    unittest.main()

On note l'usage du filtre jinja capitalize. Tous les outils de jinja sont à disposition, ce serait con de s'en priver.

Pour utiliser le template on fait $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git, ce qui nous donne :

Clonage dans 'cookiecutter-pypackage'...
remote: Counting objects: 505, done.
remote: Total 505 (delta 0), reused 0 (delta 0), pack-reused 505
Réception d'objets: 100% (505/505), 77.89 KiB | 0 bytes/s, done.
Résolution des deltas: 100% (265/265), done.
Vérification de la connectivité... fait.
full_name (default is "Audrey Roy")? Sam
email (default is "audreyr@gmail.com")? lesametlemax@gmail.com
github_username (default is "audreyr")? sam
project_name (default is "Python Boilerplate")? essai
repo_name (default is "boilerplate")? essai
project_short_description (default is "Python Boilerplate contains all the boilerplate you need to create a Python package.")? C'est un essai j'ai dis
release_date (default is "2015-01-11")? 
year (default is "2015")? 
version (default is "0.1.0")? 

Notez les valeurs par défaut du fichier JSON, qui sont celles de l'auteur.

Le résultat généré :

├── essai
│   ├── AUTHORS.rst
│   ├── CONTRIBUTING.rst
│   ├── docs
│   │   ├── authors.rst
│   │   ├── conf.py
│   │   ├── contributing.rst
│   │   ├── history.rst
│   │   ├── index.rst
│   │   ├── installation.rst
│   │   ├── make.bat
│   │   ├── Makefile
│   │   ├── readme.rst
│   │   └── usage.rst
│   ├── essai
│   │   ├── essai.py
│   │   └── __init__.py
│   ├── HISTORY.rst
│   ├── LICENSE
│   ├── Makefile
│   ├── MANIFEST.in
│   ├── README.rst
│   ├── requirements.txt
│   ├── setup.cfg
│   ├── setup.py
│   ├── tests
│   │   ├── __init__.py
│   │   └── test_essai.py
│   └── tox.ini
└── README.rst

Le fichier __init__ est devenu :

# -*- coding: utf-8 -*-
 
__author__ = 'Sam'
__email__ = 'lesametlemax@gmail.com'
__version__ = '0.1.0'

Et le fichier de test :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
"""
test_essai
----------------------------------
 
Tests for `essai` module.
"""
 
import unittest
 
from essai import essai
 
 
class TestEssai(unittest.TestCase):
 
    def setUp(self):
        pass
 
    def test_something(self):
        pass
 
    def tearDown(self):
        pass
 
if __name__ == '__main__':
    unittest.main()

L'outil accepte un beau degré de customisation, avec un fichier de configuration général au niveau de l'utilisateur (et donc des valeurs par défaut par utilisateur), une API programmable en Python, des hooks pre et post generation (qui permettent d'injecter des variables dynamiquement comme par exemple un uuid ou un timestamp).

Donc si vous recréez souvent les mêmes layout de projet encore et encore, pensez à cookiecutter.

Mais surtout, surtout, si vous codez votre propre framework, ne faites pas comme Django, utilisez l'API de cookiecutter au lieu de réinventer la roue.

10 thoughts on “Templates de projet avec cookiecutter

  • Kikoololmdr

    Merci pour le partage de cet outil.

    J’ai un script python perso qui s’occupe de générer des templates de code python, avec Jinja, qui me fait l’init de git et qui me crée le projet pour sublime text (avec les variables pour les linters et autocomplete par projet). Sauf que quand il faut le distribuer entre les machines, c’est un peu relou. Et j’ai eu la flemme de le packager correctement et du coup, je ne m’en sert quasiment pas, hormis la fois où je l’ai écris.

    Avoir une solution un peu plus propre et universel devrait me permettre de l’utiliser (!).

    Je vais jeter un oeil, et je pense qu’avec le système de hook, il doit être faisable de faire ce que je faisais pour git et sublime.

  • Kikoololmdr

    Merci pour le partage de cet outil.

    J’ai un script python perso qui s’occupe de générer des templates de code python, avec Jinja, qui me fait l’init de git et qui me crée le projet pour sublime text (avec les variables pour les linters et autocomplete par projet). Sauf que quand il faut le distribuer entre les machines, c’est un peu relou. Et j’ai eu la flemme de le packager correctement et du coup, je ne m’en sert quasiment pas, hormis la fois où je l’ai écris.

    Avoir une solution un peu plus propre et universel devrait me permettre de l’utiliser (!).

    Je vais jeter un oeil, et je pense qu’avec le système de hook, il doit être faisable de faire ce que je faisais pour git et sublime.

  • Digaboy

    @Kikoololmdr : ton histoire d’initialisation pour sublim text me plait. Tu peux développer un peu plus ?

  • Thierry

    2scoops of Django pour 1.8 est un bouquin fantastique. En dehors de la bible que ce devrait être pour tout bon dév Django, il démontre page après page qu’un bon dev est encore meilleur si elle/il est créatif.

  • Zulu

    “L’idée d’avoir de templates pour ses projets n’a rien de nouveaux, ”

  • Kikoololmdr

    @Digaboy

    J’ai oublié de préciser que c’est appelé depuis les hooks de virtualenvwrapper (d’où les appels à env dans ce qui suit)

    Le bout de code qui gère ça :

    # Création d'un projet sublime-text
    sublime_path = Path('/home/kikoololmdr/Programmation/sublime-text/') / project_folder.relpath(virtual_folder)
    sublime_path += '.sublime-project'
    sublime_path.parent.mkdir_p()
    print("Création d'un projet sublime-text %s" % sublime_path)
    with open(sublime_path, 'wb') as f:
        json.dump({
            'folders': [
                {
                    'path': project_folder,
                    'folder_exclude_patterns': ['bin', 'include', 'lib', 'local'],
                },
            ],
            "settings": {
                "python_interpreter": python_bin,
                "pylinter": {
                    "python_bin": python_bin,
                    "python_path": [
                        project_folder,
                        project_folder / 'lib' / "python{pv.major}.{pv.minor}".format(pv=python_version) / 'site-packages',
                    ],
                    "working_dir": project_folder,
                }
            }
        }, f, sort_keys=True, indent=4, separators=(',', ': '))

    Et le code en entier est là (il manque les templates, mais le script se suffit à lui-même pour la lecture rapide)

    Et j’en profite pour m’excuser pour le double commentaire, j’ai foiré le click ce matin (oui, c’est possible).

  • Ludovic Gasc (GMLudo)

    Cookiecutter est très puissant, on s’en sert pour créer des daemons à la chaine: https://github.com/Eyepea/cookiecutter-API-Hour

    Par contre, j’ai eu du retour de personnes qui m’ont dit que c’était trop compliqué ou qu’ils ne voulaient pas l’utiliser.

    Pour finir, j’ai dû faire un simple exemple dans la documentation: http://pythonhosted.org/api_hour/tutorials/all_in_one.html

    Comme quoi, même pour un “bête” outil comme cookiecutter qui est utilisé en mode one shot, la résistance au changement du Pythoniste moyen est quand même assez forte ;-)

  • Sekun

    Merci, je m’en sers maintenant abusivement pour créer différents template latex, cookiecutter est bien pensé et simple d’utilisation.

    Je n’ai par contre pas réussi à mettre une variable à None ou false pour appliquer {% if cookiecutter.variable %} dans le template.

  • Sam Post author

    Ce n’est pas une utilisation abuse, au contraire, c’est parfait pour ça. Ou pour générer du xml également plutôt que de builder tout ça avec un gros arbre dom. Met une chaine vide dans le json.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.