Skip to main content

Special Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.

Project description

Copier Template-Extensions

ci documentation pypi version

Special Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.

Installation

With pip:

pip install copier-template-extensions

With uv:

uv tool install copier --with copier-template-extensions

With pipx:

pipx install copier
pipx inject copier copier-template-extensions

Usage

In your template configuration, first add our loader extension, then add your templates extensions using relative file paths, and the class name after a colon:

_jinja_extensions:
- copier_template_extensions.TemplateExtensionLoader
- extensions/context.py:ContextUpdater
- extensions/slugify.py:SlugifyExtension

With this example, you are supposed to have an extensions directory at the root of your template containing two modules: context.py and slugify.py.

๐Ÿ“ template_root
โ”œโ”€โ”€ ๐Ÿ“„ abc.txt.jinja
โ”œโ”€โ”€ ๐Ÿ“„ copier.yml
โ””โ”€โ”€ ๐Ÿ“ extensions
 ย ย  โ”œโ”€โ”€ ๐Ÿ“„ context.py
 ย ย  โ””โ”€โ”€ ๐Ÿ“„ slugify.py

See Context hook extension to see how the ContextUpdater class can be written.

The SlugifyExtension class could be written like this:

import re
import unicodedata

from jinja2.ext import Extension


# taken from Django
# https://github.com/django/django/blob/main/django/utils/text.py
def slugify(value, allow_unicode=False):
    """
    Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
    dashes to single dashes. Remove characters that aren't alphanumerics,
    underscores, or hyphens. Convert to lowercase. Also strip leading and
    trailing whitespace, dashes, and underscores.
    """
    value = str(value)
    if allow_unicode:
        value = unicodedata.normalize('NFKC', value)
    else:
        value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
    value = re.sub(r'[^\w\s-]', '', value.lower())
    return re.sub(r'[-\s]+', '-', value).strip('-_')


class SlugifyExtension(Extension):
    def __init__(self, environment):
        super().__init__(environment)
        environment.filters["slugify"] = slugify

Context hook extension

This package also provides a convenient extension class allowing template writers to update the context used to render templates, in order to add, modify or remove items of the context.

In one of your relative path extensions modules, create a class that inherits from ContextHook, and override its hook method:

from copier_template_extensions import ContextHook


class ContextUpdater(ContextHook):
    def hook(self, context):
        context["say"] = "hello " + context["name"]

In your Jinja templates, you will now have access to the {{ say }} variable directly.

This can be extremely useful in template projects where you don't want to ask too many questions to the users and instead infer some values from their answers.

Consider the following example: you ask your users if they want to generate a CLI app or a web API. Depending on their answer, the main Python module should be named cli.py or app.py.

Without the context hook, you would need to write a Jinja macro somewhere, or update the context directly in Jinja, and import this file (still using Jinja) in the filename of the module:

{# using macros #}
{%- macro module_name() %}
  {%- if project_type == "webapi" %}app{% else %}cli{% endif %}
{%- endmacro %}
{# or enhancing the context #}
{#- Initiate context with a copy of Copier answers -#}
{%- set ctx = _copier_answers.copy() -%}

{#- Populate our new variables -#}
{%- set _ = ctx.update({"module_name": ("app" if project_type == "webapi" else "cli") -%}
๐Ÿ“ template_root
โ”œโ”€โ”€ ๐Ÿ“„ copier.yml
โ”œโ”€โ”€ ๐Ÿ“„ macros      # the macros file
โ”œโ”€โ”€ ๐Ÿ“„ context     # the context file
โ”œโ”€โ”€ ๐Ÿ“ extensions
โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ slugify.py
โ””โ”€โ”€ ๐Ÿ“ {{project_name|slugify}}
    โ”‚
    โ”‚   # using the macros
    โ”œโ”€โ”€ ๐Ÿ“„ {% import 'macros' as macros with context %}{{macros.module_name()}}.py.jinja
    โ”‚
    โ”‚   # or using the enhanced context
    โ””โ”€โ”€ ๐Ÿ“„ {% from 'context' import ctx with context %}{{ctx.module_name}}.py.jinja

As you can see, both forms are really ugly to write:

  • the macros or context can only be placed in the root, as slashes / are not allowed in filenames
  • you must use spaces and single-quotes (double-quotes are not valid filename characters on Windows) in your templated filenames, which is not clean
  • filenames are very long

Using our context hook instead makes it so easy and clean!

from copier_template_extensions import ContextHook


class ContextUpdater(ContextHook):
    def hook(self, context):
        context["module_name"] = "app" if context["project_type"] == "webapi" else "cli"
๐Ÿ“ template_root
โ”œโ”€โ”€ ๐Ÿ“„ copier.yml
โ”œโ”€โ”€ ๐Ÿ“ extensions
โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ slugify.py
โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ context.py
โ””โ”€โ”€ ๐Ÿ“ {{project_name|slugify}}
    โ””โ”€โ”€ ๐Ÿ“„ {{module_name}}.py.jinja

You can do many more things with a context hook, like downloading data from external resources to include it in the context, etc.

[!TIP] Context hooks run during every Copier rendering phase. During project generation or project updates, Copier passes through several rendering phases: when prompting (questions / answers), when rendering files, when running tasks, and when running migrations.

By default, a context hook runs during all these phases, possibly multiple times, for example once per prompted question, or once per rendered file. The task of running the hook only once, or during a specific phase only, is left to you.

To run only once, you can use caching within your class (for example by storing computed values in class variables).

To run during a specific phase only, you can check the value of context["_copier_phase"] (Copier 9.6+), which is one of: "prompt", "render", "tasks", "migrate".

Other key-value pairs can be found in the context that you might find useful (Copier configuration, etc.).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

copier_template_extensions-0.3.3.tar.gz (35.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

copier_template_extensions-0.3.3-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file copier_template_extensions-0.3.3.tar.gz.

File metadata

File hashes

Hashes for copier_template_extensions-0.3.3.tar.gz
Algorithm Hash digest
SHA256 86bd5e2e89c94b6f9321e4078781b4456c5658b820bcae49ac60fd1e1dd23c10
MD5 3928d4997d03c9bf2deb8137d6f6cc8c
BLAKE2b-256 8a471a6638b4323a9eba2f19f4b1364bd77fcdb2b525a1c1cccb1f73422ff97e

See more details on using hashes here.

File details

Details for the file copier_template_extensions-0.3.3-py3-none-any.whl.

File metadata

File hashes

Hashes for copier_template_extensions-0.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 bf6bbdebada26132640d6e1128723fa1ff7eb59d008de07a7543c51e0facb0ea
MD5 40ad12f8fcaf0ac059c0a5156a18111b
BLAKE2b-256 c6da117372469b9bb1555ae7f8a0b1f3e00345044e6006c50f3f6d3cb20c9843

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page