July 28, 2019

How to scaffold Django projects with Cookiecutter

This post is a guide on how to scaffold (quick-start) new projects efficiently with Cookiecutter, a library that creates projects from project-templates. It outlines how I created my own Django cookiecutter, Scaffold Django X, but the same can be applied to Flask and pretty much any other Python project.

Working on some Django articles, I found myself needing to start a new project more often than usual. It can get tedious: having to initialize a project, filling boilerplate settings, adding template-files and directories…the list goes on.

Sure, you can duplicate an existing project. But then too much time is spent on removing unneeded files, figuring out why the new project isn’t running (you forgot to remove a file or a line), and fiddling with settings. Might as well just start from scratch.

Or, use Cookiecutter:

What is Cookiecutter?

Cookiecutter is a command-line utility that creates projects from project-templates, aptly called cookiecutters. It allows for dynamic insertion of content within files and inclusion/exclusion of the files themselves in a way that makes project generation flexible and convenient.

Cookiecutter (the library) shares the same name with what it can be used to generate, a cookiecutter. This may be confusing at times. Keep in mind that Cookiecutter the library is written with a big C.

After you install Cookiecutter you can clone any local or remote project-template, choose how to configure your project from a set of options (predefined by the template’s author), and you’re ready to go. Want to use Celery? it’s automatically included in the requirements.txt file, its relevant configs are added to Django’s settings.py, and Celery-specific files are already in the generated project’s tree. Using Django as a backend only? a cookiecutter can remove the static and templates folders for you.

The most popular Django related Cookiecutter project is Cookiecutter Django by Daniel Roy Greenfeld. It’s very customizable and includes a long list of options and features. I found Cookiecutter Django too opinionated (and a bit of an overkill) for my use-case so I created my own cookiecutter: Scaffold Django X.

Generating a project from a cookiecutter

Before creating a cookiecutter, it’s first worth understanding how you’d generate a project from an existing one.

Because you would want to be able to use it from any folder, it’s a good idea to install Cookiecutter in your global/main Python environment:

$ pip install cookiecutter

If you want to scaffold a project from a local cookiecutter, navigate to the folder in which you want your actual project to live, open Terminal, and type cookiecutter followed by the path to the cookiecutter that you want to base your project on.

To create a project in ~/my-projects/ based on the cookiecutter/template called simple-django-cookiecutter, you go to ~/my-projects/:

$ cd ~/my-projects/

This is where Cookiecutter will place the generated project folder. You then invoke cookiecutter, specifying the path to the project template:

$ cookiecutter ~/code/my-cookiecutters/simple-django-cookiecutter/

You will then be prompted to fill-in some details that will be used to populate your project with the relevant configurations. These configuration variables and their default values are derived from a special kind of file, cookiecutter.json, which is covered later in this guide.

For example, the simple cookiecutter that I’ve created prompts for the following options when invoked:

project_slug [open_folder]:

project_name [Open Folder]:

description [A very nice weblog]:

author_name [SKM]:

author_email [openfolder@example.com]:

include_jquery_cdn [n]:

Select css_framework:
1 - tailwindcss
2 - bootstrap
3 - none

Choose from 1, 2, 3 (1, 2, 3) [1]:

If the project_slug is provided as music_app, then this is what the project’s root folder will be called. The description will automatically go in the website’s meta tags. include_jquery_cdn is handled in a similar fashion: if y is provided instead of the default n, then a <link> to jQuery’s CDN is inserted in the project’s base.html. Django Cookiecutter X also populates base.html with an empty main.css and main.js files so that they’re ready to use. I plan to extend it in the future with more options and will notify newsletter subscribers of major additions.

At the end of this process, a music_app folder is placed under ~/my-projects and you can start developing the preconfigured Django project.

It’s also possible to clone remote cookiecutters. From Github for example:

cookiecutter https://github.com/SHxKM/django-scaffold-cookiecutter

This is how one would generate a project from a template. Next: how to build the template itself.

How to create a cookiecutter: the basics

The minimum requirement for a valid project-template is that it contains a cookiecutter.json file at its root folder. This file is used to define the different variables the user has to fill or choose from during the generation stage. It also sets an overridable default for each variable:

{
    "some_variable": "some_default_value",
    "project_slug": "open_folder",
    "project_name": "Open Folder",
    "description": "A very nice weblog",
    "author_name": "SKM",
    "author_email": "openfolder@example.com",
    "include_jquery_cdn": "n",
    "css_framework": [
        "tailwindcss",
        "bootstrap",
        "none"
    ]
}

These are key-value pairs, with the each value denoting the default to use. If a user simply hits enter when prompted for the project_name, it will be Open Folder. If a list is used — like in css_framework above — Cookiecutter will present a numbered choice prompt for that option.

So, when the user has finished answering all prompts, Cookiecutter then scans the files and looks for blocks that match each of the keys above. But where and how are these values used?

Variables in filenames and folders

Here’s the root folder of an example cookiecutter:

.
├── cookiecutter.json
└── {{ cookiecutter.project_slug }}
    ├── Pipfile
    ├── manage.py
    ├── static
    ├── templates
    └── {{ cookiecutter.project_slug }}

So for one, filenames and directories can themselves be variables. The Django project lives under the directory {{ cookiecutter.project_slug }}. The directory is named this way because Cookiecutter is going to dynamically rename it when the project is generated. You may be familiar with this curly-brace notation as Cookiecutter uses the same Jinja2 templating engine that Django supports.

Variables in HTML files

Here’s a snippet from base.html:

{# base.html #}
<head>
...
  <title>{{ cookiecutter.project_name }}</title>
  <meta name="description" content="{{ cookiecutter.description }}">
...
</head>

When the project is generated, {{ cookiecutter.project_name }} is simply replaced with the name provided by the user in the Terminal.

The fact that Cookiecutter uses the same syntax as Django templates can create a problem if it tries to parse Django’s own tags, like {% static %} or {% url %}. You can escape these with the {% raw %} and {% endraw %} tags:

{% raw %}{% load static %}{% endraw %}

For conditionals, like whether to include the jQuery CDN, an if block is employed:

{%- if cookiecutter.include_jquery_cdn == "y" -%}
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
{%- endif %}

Variables in Python files

That’s basically the gist of it for template files, but the same logic can be used in Python files. Here’s a snippet from Django’s settings.py:

# settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    {%- if cookiecutter.css_framework == 'tailwindcss' -%}
    "tailwind",
    "theme",
    {%- endif %}    
]

The above isn’t valid Python but these tags are stripped-out anyway when the project is generated. If the user picks Tailwind CSS as their CSS framework then we need to include some extra lines (apps) in INSTALLED_APPS.

Here’s how you’d access a cookiecutter’s variable inside a Python file:

project_slug = "{{ cookiecutter.project_slug }}"

Variables in…cookiecutter.json

We can make our cookiecutter.json smarter by deriving project_slugs default value from project_name1:

{
    "project_name": "Open Folder",
    "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}"
} 

This way, if the user enters My App” as their project_name, the default value for project_slug becomes my_app. The user can then simply hit enter to use this value for the slug, or override it as they wish.

Ignoring files when parsing

We can tell Cookiecutter to ignore — not attempt to parse — certain directories or files:

{
    "project_slug": "open_folder",
    "project_name": "Open Folder",
    "_copy_without_render": [
        "theme"
    ]
}

_copy_without_render tells Cookiecutter to copy files as-is” without attempting to render (parse) them. In the case above, theme is a folder that contains a package that integrates the Tailwind CSS framework into Django. It contains 3rd-party files that should remain untouched even if they contain curly braces {{ }} that Cookiecutter usually sniffs for and strips.

Pre/Post-generate hooks

Cookiecutter also supports pre- and post-generation hooks. These are just regular Python files that are run before/after the project is generated. They are named pre_gen_project.py and post_gen_project.py, respectively. You place them inside a directory named hooks at the root of the project:

├── cookiecutter.json
├── hooks
│   ├── post_gen_project.py
│   └── pre_gen_project.py
└── {{ cookiecutter.project_slug }}
    ├── Pipfile
    ├── manage.py
    ├── static
    ├── templates
    ...

These generation hooks can be extremely useful when files (not just lines of code) should be added/removed dynamically depending on the user input. Below are examples of how each of these hooks can be useful.

Pre-generation hooks

If you want to validate that the project_slug given by the user is all lower-case, you can create a file hooks/pre_gen_project.py and include the following:

# hooks/pre_gen_project.py
project_slug = "{{ cookiecutter.project_slug }}"

assert (
    project_slug == project_slug.lower()
), f"{project_slug} project slug should be all lowercase"

Before Cookiecutter attempts to parse the project files, it will run pre_gen_project.py and if the user provided a slug that isn’t all lowercase, the assertion will fail. The project isn’t generated at all and an appropriate error message is displayed.

Post-generation hooks

We can do some interesting things in post_gen_project.py as well. Remember the aforementioned theme folder? it contains necessary files and modules for the 3rd party package django-tailwind. But if the user chose bootstrap or none we don’t need this directory anymore:

# hooks/post_gen_project.py
import os
import shutil


def remove_tailwind_folder():
    theme_dir_path = "theme"
    if os.path.exists(theme_dir_path):
        shutil.rmtree(theme_dir_path)

# ...

def main():
    if "{{cookiecutter.css_framework}}".lower() != "tailwindcss":
        remove_tailwind_folder()

if __name__ == "__main__":
    main()

main() checks if the user chose to use Tailwind CSS, and if not, calls the function remove_tailwind_folder() which will delete its folders. As you can see, we have access to project variables in the generation hooks files:

if "{{cookiecutter.css_framework}}".lower() != "tailwindcss":
   # variable key                            # variable value

Conclusion

Cookiecutter can cut project generation time significantly. For more complex boilerplate the time savings can be reduced by more than 90%. As always, the package’s docs site is a good place to start if there’s something you’re unsure of.

Once I dig further this guide will be appended with a section about automated-testing of cookiecutters. An update will be sent to subscribers of the newsletter.


  1. (full-credit for this to the aforementioned Cookiecutter Django)


Previous post
Eliminating indentation by returning early Returning early is a fairly basic but useful technique and it’s one that I’ve only adopted relatively late in my Python journey. The Zen of Python