Skip to content

cookiecutter

Cookiecutter is a command-line utility that creates projects from cookiecutters (project templates).

Install

pip install cookiecutter

Use

DEPRECATION: use cruft instead

You may want to use cruft to generate your templates instead, as it will help you maintain the project with the template updates. Something that it's not easy with cookiecutter alone

cookiecutter {{ path_or_url_to_cookiecutter_template }}

User config

If you use Cookiecutter a lot, you’ll find it useful to have a user config file. By default Cookiecutter tries to retrieve settings from a .cookiecutterrc file in your home directory.

Example user config:

default_context:
    full_name: "Audrey Roy"
    email: "audreyr@example.com"
    github_username: "audreyr"
cookiecutters_dir: "/home/audreyr/my-custom-cookiecutters-dir/"
replay_dir: "/home/audreyr/my-custom-replay-dir/"
abbreviations:
    python: https://github.com/audreyr/cookiecutter-pypackage.git
    gh: https://github.com/{0}.git
    bb: https://bitbucket.org/{0}

Possible settings are:

default_context
A list of key/value pairs that you want injected as context whenever you generate a project with Cookiecutter. These values are treated like the defaults in cookiecutter.json, upon generation of any project.
cookiecutters_dir
Directory where your cookiecutters are cloned to when you use Cookiecutter with a repo argument.
replay_dir
Directory where Cookiecutter dumps context data to, which you can fetch later on when using the replay feature.
abbreviations
A list of abbreviations for cookiecutters. Abbreviations can be simple aliases for a repo name, or can be used as a prefix, in the form abbr:suffix. Any suffix will be inserted into the expansion in place of the text {0}, using standard Python string formatting. With the above aliases, you could use the cookiecutter-pypackage template simply by saying cookiecutter python.

Write your own cookietemplates

Create files or directories with conditions

For files use a filename like '{{ ".vault_pass.sh" if cookiecutter.vault_pass_entry != "None" else "" }}'.

For directories I haven't yet found a nice way to do it (as the above will fail), check the issue or the hooks documentation for more information.

File: post_gen_project.py

import os
import sys

REMOVE_PATHS = [
    '{% if cookiecutter.packaging != "pip" %} requirements.txt {% endif %}',
    '{% if cookiecutter.packaging != "poetry" %} poetry.lock {% endif %}',
]

for path in REMOVE_PATHS:
    path = path.strip()
    if path and os.path.exists(path):
        if os.path.isdir(path):
            os.rmdir(path)
        else:
            os.unlink(path)

Add some text to a file if a condition is met

Use jinja2 conditionals. Note the - at the end of the conditional opening, play with {%- ... -%} and {% ... %} for different results on line appending.

{% if cookiecutter.install_docker == 'yes' -%}
- src: git+ssh://mywebpage.org/ansible-roles/docker.git
  version: 1.0.3
{%- else -%}
- src: git+ssh://mywebpage.org/ansible-roles/other-role.git
  version: 1.0.2
{%- endif %}

Initialize git repository on the created cookiecutter

Added the following to the post generation hooks.

File: hooks/post_gen_project.py

import subprocess

subprocess.call(['git', 'init'])
subprocess.call(['git', 'add', '*'])
subprocess.call(['git', 'commit', '-m', 'Initial commit'])

Prevent cookiecutter from processing some files

By default cookiecutter will try to process every file as a Jinja template. This behaviour produces wrong results if you have Jinja templates that are meant to be taken as literal. Starting with cookiecutter 1.1, you can tell cookiecutter to only copy some files without interpreting them as Jinja templates.

Add a _copy_without_render key in the cookiecutter config file (cookiecutter.json). It takes a list of regular expressions. If a filename matches the regular expressions it will be copied and not processed as a Jinja template.

{
    "project_slug": "sample",
    "_copy_without_render": [
        "*.js",
        "not_rendered_dir/*",
        "rendered_dir/not_rendered_file.ini"
    ]
}

Prevent additional whitespaces when jinja condition is not met.

Jinja2 has a whitespace control that can be used to manage the whitelines existent between the Jinja blocks. The problem comes when a condition is not met in an if block, in that case, Jinja adds a whitespace which will break most linters.

This is the solution I've found out that works as expected.

### Multienvironment

This playbook has support for the following environments:

{% if cookiecutter.production_environment == "True" -%}
* Production
{% endif %}
{%- if cookiecutter.staging_environment == "True" -%}
* Staging
{% endif %}
{%- if cookiecutter.development_environment == "True" -%}
* Development
{% endif %}
### Tags

Testing your own cookiecutter templates

The pytest-cookies plugin comes with a cookies fixture which is a wrapper for the cookiecutter API for generating projects. It helps you verify that your template is working as expected and takes care of cleaning up after running the tests.

Install

pip install pytest-cookies

Usage

@pytest.fixture def context(): return { "playbook_name": "My Test Playbook", }

The cookies.bake() method generates a new project from your template based on the default values specified in cookiecutter.json:

def test_bake_project(cookies):
    result = cookies.bake(extra_context={'repo_name': 'hello world'})

    assert result.exit_code == 0
    assert result.exception is None
    assert result.project.basename == 'hello world'
    assert result.project.isdir()

It accepts the extra_context keyword argument that is passed to cookiecutter. The given dictionary will override the default values of the template context, allowing you to test arbitrary user input data.

The cookiecutter-django has a nice test file using this fixture.

Mocking the contents of the cookiecutter hooks

Sometimes it's interesting to add interactions with external services in the cookiecutter hooks, for example to activate a CI pipeline.

If you want to test the cookiecutter template you need to mock those external interactions. But it's difficult to mock the contents of the hooks because their contents aren't run by the cookies.bake() code. Instead it delegates in cookiecutter to run them, which opens a subprocess to run them, so the mocks don't work.

The alternative is setting an environmental variable in your tests to skip those steps:

File: tests/conftest.py

import os

os.environ["COOKIECUTTER_TESTING"] = "true"

File: hooks/pre_gen_project.py

def main():
    # ... pre_hook content ...


if __name__ == "__main__":

    if os.environ.get("COOKIECUTTER_TESTING") != "true":
        main()

If you want to test the content of main, you can now mock each of the external interactions. But you'll face the problem that these files are jinja2 templates of python files, so it's tricky to test them, due to syntax errors.

Debug failing template generation

Sometimes the generation of the templates will fail in the tests, I've found that the easier way to debug why is to inspect the result object of the result = cookies.bake() statement with pdb.

It has an exception method with lineno argument and source. With that information I've been able to locate the failing line. It also has a filename attribute but it doesn't seem to work for me.

References


Last update: 2021-12-09
Back to top