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 Templates Extensions

ci documentation pypi version gitpod gitter

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-templates-extensions

With pipx:

pip install --user pipx

pipx install copier
pipx inject copier copier-templates-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_templates_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_templates_extensions import ContextHook


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

Using the above example, your context will be updated with the new_context returned by the method. If you prefer to modify the context in-place instead, for example to remove items from it, set the update class attribute to False:

from copier_templates_extensions import ContextHook


class ContextUpdater(ContextHook):
    update = False

    def hook(self, context):
        context["say"] = "hello " + context["name"]
        del 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_templates_extensions import ContextHook


class ContextUpdater(ContextHook):
    def hook(self, context):
        return {"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

How does it work?

Beware the ugly hack! Upon loading the special loader extension, the function responsible for importing a Python object using its dotted-path (a string) is patched in the jinja.environment module, where it's used to load extensions. The patched version adds support for loading extensions using relative file paths. The file system loader of the Jinja environment and its searchpaths attribute are used to find the local clone of the template and determine the absolute path of the extensions to load.

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-templates-extensions-0.3.0.tar.gz (12.1 kB view details)

Uploaded Source

Built Distribution

File details

Details for the file copier-templates-extensions-0.3.0.tar.gz.

File metadata

File hashes

Hashes for copier-templates-extensions-0.3.0.tar.gz
Algorithm Hash digest
SHA256 48b3f5213ee913b931df40e4e9325b58b9c9a741f1052a7da1b247ed991f5150
MD5 ffd177b2eba2b18e64d2c87545c31030
BLAKE2b-256 ac0d3e047d28ffe6712e6936a72d8ae288a0e24e7995c645b011db82edb7fb3b

See more details on using hashes here.

File details

Details for the file copier_templates_extensions-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for copier_templates_extensions-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 733e709536e03768b1201c79525534d7fd153932cf764492a0396692e513f244
MD5 068450be6a57ff5d140a200ce4e70224
BLAKE2b-256 03937ebfcbe54410f2d43c8bb8bda1910f7d921b2d5efc52289718969ab0a63d

See more details on using hashes here.

Supported by

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