cookiecutter/cookiecutter

View on GitHub
docs/advanced/hooks.rst

Summary

Maintainability
Test Coverage
Hooks
=====

Cookiecutter hooks are scripts executed at specific stages during the project generation process. They are either Python or shell scripts, facilitating automated tasks like data validation, pre-processing, and post-processing. These hooks are instrumental in customizing the generated project structure and executing initial setup tasks.

Types of Hooks
--------------

+------------------+------------------------------------------+------------------------------------------+--------------------+----------+
| Hook             | Execution Timing                         | Working Directory                        | Template Variables | Version  |
+==================+==========================================+==========================================+====================+==========+
| pre_prompt       | Before any question is rendered.         | A copy of the repository directory       | No                 | 2.4.0    |
+------------------+------------------------------------------+------------------------------------------+--------------------+----------+
| pre_gen_project  | After questions, before template process.| Root of the generated project            | Yes                | 0.7.0    |
+------------------+------------------------------------------+------------------------------------------+--------------------+----------+
| post_gen_project | After the project generation.            | Root of the generated project            | Yes                | 0.7.0    |
+------------------+------------------------------------------+------------------------------------------+--------------------+----------+

Creating Hooks
--------------

Hooks are added to the ``hooks/`` folder of your template. Both Python and Shell scripts are supported.

**Python Hooks Structure:**

.. code-block::

    cookiecutter-something/
    ├── {{cookiecutter.project_slug}}/
    ├── hooks
    │   ├── pre_prompt.py
    │   ├── pre_gen_project.py
    │   └── post_gen_project.py
    └── cookiecutter.json

**Shell Scripts Structure:**

.. code-block::

    cookiecutter-something/
    ├── {{cookiecutter.project_slug}}/
    ├── hooks
    │   ├── pre_prompt.sh
    │   ├── pre_gen_project.sh
    │   └── post_gen_project.sh
    └── cookiecutter.json

Python scripts are recommended for cross-platform compatibility. However, shell scripts or `.bat` files can be used for platform-specific templates.

Hook Execution
--------------

Hooks should be robust and handle errors gracefully. If a hook exits with a nonzero status, the project generation halts, and the generated directory is cleaned.

**Working Directory:**

* ``pre_prompt``: Scripts run in the root directory of a copy of the repository directory. That allows the rewrite of ``cookiecutter.json`` to your own needs.

* ``pre_gen_project`` and ``post_gen_project``: Scripts run in the root directory of the generated project, simplifying the process of locating generated files using relative paths.

**Template Variables:**

The ``pre_gen_project`` and ``post_gen_project`` hooks support Jinja template rendering, similar to project templates. For instance:

.. code-block:: python

    module_name = '{{ cookiecutter.module_name }}'

Examples
--------

**Pre-Prompt Sanity Check:**

A ``pre_prompt`` hook, like the one below in ``hooks/pre_prompt.py``, ensures prerequisites, such as Docker, are installed before prompting the user.

.. code-block:: python

    import sys
    import subprocess

    def is_docker_installed() -> bool:
        try:
            subprocess.run(["docker", "--version"], capture_output=True, check=True)
            return True
        except Exception:
            return False

    if __name__ == "__main__":
        if not is_docker_installed():
            print("ERROR: Docker is not installed.")
            sys.exit(1)

**Validating Template Variables:**

A ``pre_gen_project`` hook can validate template variables. The following script checks if the provided module name is valid.

.. code-block:: python

    import re
    import sys

    MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$'
    module_name = '{{ cookiecutter.module_name }}'

    if not re.match(MODULE_REGEX, module_name):
        print(f'ERROR: {module_name} is not a valid Python module name!')
        sys.exit(1)

**Conditional File/Directory Removal:**

A ``post_gen_project`` hook can conditionally control files and directories. The example below removes unnecessary files based on the selected packaging option.

.. code-block:: python

    import os

    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):
            os.unlink(path) if os.path.isfile(path) else os.rmdir(path)