π€ Luca Baggi
πΌ ML Engineer @Futura
π Organiser @Python Milano
cookiecutter
?cookiecutter
recipeπ€ Luca Baggi
πΌ ML Engineer @Futura
π Organiser @Python Milano
cookiecutter
?cookiecutter
recipecookiecutter
?How many boxes do you check?
π I want to put in place architectural decisions
π I need flexible and interactive project templates
π I wish to reduce boilerplate:
pyproject.toml
setup.cfg
setup.py
.pre-commit-config.yaml
Makefile
.env
cookiecutter
should be installed globally# the old way pip install -U cookiecutter # a bit like npx pipx install cookiecutter
# the old way pip install -U cookiecutter # a bit like npx pipx install cookiecutter
pipx
# macos brew install pipx pipx ensurepath # linux python3 -m pip install --user pipx python3 -m pipx ensurepath # windows # If you installed python using the app-store, replace `python` with `python3` in the next line. python -m pip install --user pipx pipx ensurepath
# macos brew install pipx pipx ensurepath # linux python3 -m pip install --user pipx python3 -m pipx ensurepath # windows # If you installed python using the app-store, replace `python` with `python3` in the next line. python -m pip install --user pipx pipx ensurepath
cookiecutter
recipeSome minimal scaffolding
mkdir my-cookiecutter
mkdir my-cookiecutter
gh repo create my-cookiecutter git init git remote add origin <gh-username>/my-cookiecutter
gh repo create my-cookiecutter git init git remote add origin <gh-username>/my-cookiecutter
cookiecutter.json
π:touch cookiecutter.json
touch cookiecutter.json
cookiecutter.json
, using cookiecutter
recipeHow to define variables
Define some placeholder variables whose values will be replaced when the project is created:
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
cookiecutter
recipeHow to define variables
You can add default valuesβ¦
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
cookiecutter
recipeHow to define variables
β¦ multiple choice variables (the default value is the first of the list)β¦
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
cookiecutter
recipeHow to define variables
β¦ and modify variables using Python string methods:
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
cookiecutter
recipeHow to define variables
Finally, you can define private variables:
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
{ "full_name": "", "email": "", "github_username": "", "project_name": "", "license": [ "MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0" ], "__repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", "package_name": "{{ cookiecutter.__repo_name.lower().replace('-', '_') }}", "package_version": "0.1.0", "python_version": "3.9.13", "__py_major_minor": "{{ '.'.join(cookiecutter.python_version.split(sep='.')[:-1]) }}", "use_pyenv": [ "yes", "no" ], "use_jupyter": [ "no", "yes" ], "has_documentation": [ "no", "yes" ] }
Use {{ cookiecutter.<variable> }}
where you want a substitution to happen.
It can be a filenameβ¦
. βββ {{ cookiecutter.__repo_name }} β βββ docs β βββ notebooks β βββ src β β βββ {{ cookiecutter.package_name }} β βββ tests β β βββ __init__.py β βββ .env.template β βββ .gitignore β βββ .pre-commit-config.yaml β βββ .python-version β βββ LICENSE β βββ pyproject.toml β βββ README.md βββ cookiecutter.json
. βββ {{ cookiecutter.__repo_name }} β βββ docs β βββ notebooks β βββ src β β βββ {{ cookiecutter.package_name }} β βββ tests β β βββ __init__.py β βββ .env.template β βββ .gitignore β βββ .pre-commit-config.yaml β βββ .python-version β βββ LICENSE β βββ pyproject.toml β βββ README.md βββ cookiecutter.json
Use {{ cookiecutter.<variable> }}
where you want a substitution to happen.
β¦or text inside a filename:
# pyproject.toml [project] name = "{{ cookiecutter.package_name }}" authors = [ {name = "{{ cookiecutter.full_name }}", email = "{{ cookiecutter.email }}"}, ] description = "" license = {text = "{{ cookiecutter.license }}"} readme = "README.md" dynamic = ["version"] requires-python = ">={{ cookiecutter.__py_major_minor }}" [tool.pdm] version = {source = "file", path = "src/{{ cookiecutter.package_name }}/__init__.py"}
# pyproject.toml [project] name = "{{ cookiecutter.package_name }}" authors = [ {name = "{{ cookiecutter.full_name }}", email = "{{ cookiecutter.email }}"}, ] description = "" license = {text = "{{ cookiecutter.license }}"} readme = "README.md" dynamic = ["version"] requires-python = ">={{ cookiecutter.__py_major_minor }}" [tool.pdm] version = {source = "file", path = "src/{{ cookiecutter.package_name }}/__init__.py"}
Use {{ cookiecutter.<variable> }}
where you want a substitution to happen.
It even supports conditional statements:
{%- if cookiecutter.license == "MIT" -%} The MIT License (MIT) {%- elif cookiecutter.license == "BSD-3" -%} The BSD3 License (BSD3) {%- elif cookiecutter.license == "GNU GPL v3.0" -%} The GPLv3 License (GPLv3) {%- elif cookiecutter.license == "Apache Software License 2.0" -%} The APACHE License (APACHE) {% endif %}
{%- if cookiecutter.license == "MIT" -%} The MIT License (MIT) {%- elif cookiecutter.license == "BSD-3" -%} The BSD3 License (BSD3) {%- elif cookiecutter.license == "GNU GPL v3.0" -%} The GPLv3 License (GPLv3) {%- elif cookiecutter.license == "Apache Software License 2.0" -%} The APACHE License (APACHE) {% endif %}
(Thatβs a bunch of Jinja
under the hood!)
Configure cookiecutter
.zshenv
/.zshrc
:if hash cookiecutter 2>/dev/null; then export COOKIECUTTER_CONFIG="$XDG_CONFIG_HOME/cookiecutter.yaml" fi
if hash cookiecutter 2>/dev/null; then export COOKIECUTTER_CONFIG="$XDG_CONFIG_HOME/cookiecutter.yaml" fi
Configure cookiecutter
cookiecutter.yaml
configs:default_context: # default values for commonly used variables full_name: "<Name Surname>" email: "<your-github-email>" github_username: "<your-github-username>" cookiecutters_dir: "$XDG_CACHE_HOME/cookiecutters/" # where cookiecutters will be stored abbreviations: # gh (github), bb (bitbucket), and gl (gitlab) abbreviations are builtin gh: https://github.com/{0}.git py: https://github.com/baggiponte/cookiecutter-python.git
default_context: # default values for commonly used variables full_name: "<Name Surname>" email: "<your-github-email>" github_username: "<your-github-username>" cookiecutters_dir: "$XDG_CACHE_HOME/cookiecutters/" # where cookiecutters will be stored abbreviations: # gh (github), bb (bitbucket), and gl (gitlab) abbreviations are builtin gh: https://github.com/{0}.git py: https://github.com/baggiponte/cookiecutter-python.git
Add pre- and post-generation hooks
Just create a hooks
level folder at the same level of the cookiecutter.json
:
. βββ hooks β βββ post_gen_project.py βββ {{ cookiecutter.__repo_name }} βββ cookiecutter.json
. βββ hooks β βββ post_gen_project.py βββ {{ cookiecutter.__repo_name }} βββ cookiecutter.json
Add pre- and post-generation hooks
Just create a hooks
level folder at the same level of the cookiecutter.json
:
#!/usr/bin/env python import os import shutil PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) def remove_dir(filepath): shutil.rmtree(os.path.join(PROJECT_DIRECTORY, filepath)) if __name__ == '__main__': if '{{ cookiecutter.has_documentation }}' != 'yes': remove_dir('docs') if '{{ cookiecutter.use_jupyter }}' != 'yes': remove_dir('notebooks')
#!/usr/bin/env python import os import shutil PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) def remove_dir(filepath): shutil.rmtree(os.path.join(PROJECT_DIRECTORY, filepath)) if __name__ == '__main__': if '{{ cookiecutter.has_documentation }}' != 'yes': remove_dir('docs') if '{{ cookiecutter.use_jupyter }}' != 'yes': remove_dir('notebooks')