copier-template-extensions


Namecopier-template-extensions JSON
Version 0.3.3 PyPI version JSON
download
home_pageNone
SummarySpecial Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.
upload_time2025-07-15 10:40:41
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseNone
keywords copier templates extension
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Copier Template-Extensions

[![ci](https://github.com/copier-org/copier-template-extensions/workflows/ci/badge.svg)](https://github.com/copier-org/copier-template-extensions/actions?query=workflow%3Aci)
[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://copier-org.github.io/copier-template-extensions/)
[![pypi version](https://img.shields.io/pypi/v/copier-template-extensions.svg)](https://pypi.org/project/copier-template-extensions/)

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:

```bash
pip install copier-template-extensions
```

With uv:

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

With pipx:

```bash
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:

```yaml
_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](#context-hook-extension)
to see how the `ContextUpdater` class can be written.

The `SlugifyExtension` class could be written like this:

```python
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:

```python
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*:

```jinja
{# using macros #}
{%- macro module_name() %}
  {%- if project_type == "webapi" %}app{% else %}cli{% endif %}
{%- endmacro %}
```

```jinja
{# 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!**

```python
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.).

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "copier-template-extensions",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "copier, templates, extension",
    "author": null,
    "author_email": "=?utf-8?q?Timoth=C3=A9e_Mazzucotelli?= <dev@pawamoy.fr>",
    "download_url": "https://files.pythonhosted.org/packages/8a/47/1a6638b4323a9eba2f19f4b1364bd77fcdb2b525a1c1cccb1f73422ff97e/copier_template_extensions-0.3.3.tar.gz",
    "platform": null,
    "description": "# Copier Template-Extensions\n\n[![ci](https://github.com/copier-org/copier-template-extensions/workflows/ci/badge.svg)](https://github.com/copier-org/copier-template-extensions/actions?query=workflow%3Aci)\n[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://copier-org.github.io/copier-template-extensions/)\n[![pypi version](https://img.shields.io/pypi/v/copier-template-extensions.svg)](https://pypi.org/project/copier-template-extensions/)\n\nSpecial Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.\n\n## Installation\n\nWith pip:\n\n```bash\npip install copier-template-extensions\n```\n\nWith uv:\n\n```bash\nuv tool install copier --with copier-template-extensions\n```\n\nWith pipx:\n\n```bash\npipx install copier\npipx inject copier copier-template-extensions\n```\n\n## Usage\n\nIn your template configuration,\nfirst add our loader extension,\nthen add your templates extensions\nusing relative file paths,\nand the class name after a colon:\n\n```yaml\n_jinja_extensions:\n- copier_template_extensions.TemplateExtensionLoader\n- extensions/context.py:ContextUpdater\n- extensions/slugify.py:SlugifyExtension\n```\n\nWith this example, you are supposed to have an `extensions`\ndirectory at the root of your template containing two modules:\n`context.py` and `slugify.py`.\n\n```\n\ud83d\udcc1 template_root\n\u251c\u2500\u2500 \ud83d\udcc4 abc.txt.jinja\n\u251c\u2500\u2500 \ud83d\udcc4 copier.yml\n\u2514\u2500\u2500 \ud83d\udcc1 extensions\n \u00a0\u00a0 \u251c\u2500\u2500 \ud83d\udcc4 context.py\n \u00a0\u00a0 \u2514\u2500\u2500 \ud83d\udcc4 slugify.py\n```\n\nSee [Context hook extension](#context-hook-extension)\nto see how the `ContextUpdater` class can be written.\n\nThe `SlugifyExtension` class could be written like this:\n\n```python\nimport re\nimport unicodedata\n\nfrom jinja2.ext import Extension\n\n\n# taken from Django\n# https://github.com/django/django/blob/main/django/utils/text.py\ndef slugify(value, allow_unicode=False):\n    \"\"\"\n    Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated\n    dashes to single dashes. Remove characters that aren't alphanumerics,\n    underscores, or hyphens. Convert to lowercase. Also strip leading and\n    trailing whitespace, dashes, and underscores.\n    \"\"\"\n    value = str(value)\n    if allow_unicode:\n        value = unicodedata.normalize('NFKC', value)\n    else:\n        value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')\n    value = re.sub(r'[^\\w\\s-]', '', value.lower())\n    return re.sub(r'[-\\s]+', '-', value).strip('-_')\n\n\nclass SlugifyExtension(Extension):\n    def __init__(self, environment):\n        super().__init__(environment)\n        environment.filters[\"slugify\"] = slugify\n```\n\n### Context hook extension\n\nThis package also provides a convenient extension class\nallowing template writers to update the context used\nto render templates, in order to add, modify or remove\nitems of the context.\n\nIn one of your relative path extensions modules,\ncreate a class that inherits from `ContextHook`,\nand override its `hook` method:\n\n```python\nfrom copier_template_extensions import ContextHook\n\n\nclass ContextUpdater(ContextHook):\n    def hook(self, context):\n        context[\"say\"] = \"hello \" + context[\"name\"]\n```\n\nIn your Jinja templates, you will now have access\nto the `{{ say }}` variable directly.\n\nThis can be extremely useful in template projects\nwhere you don't want to ask too many questions to the users\nand instead infer some values from their answers.\n\nConsider the following example:\nyou ask your users if they want to generate\na CLI app or a web API.\nDepending on their answer,\nthe main Python module should be named\n`cli.py` or `app.py`.\n\nWithout the context hook,\nyou would need to write a Jinja macro somewhere,\nor update the context directly in Jinja,\nand import this file (still using Jinja)\n*in the filename of the module*:\n\n```jinja\n{# using macros #}\n{%- macro module_name() %}\n  {%- if project_type == \"webapi\" %}app{% else %}cli{% endif %}\n{%- endmacro %}\n```\n\n```jinja\n{# or enhancing the context #}\n{#- Initiate context with a copy of Copier answers -#}\n{%- set ctx = _copier_answers.copy() -%}\n\n{#- Populate our new variables -#}\n{%- set _ = ctx.update({\"module_name\": (\"app\" if project_type == \"webapi\" else \"cli\") -%}\n```\n\n```\n\ud83d\udcc1 template_root\n\u251c\u2500\u2500 \ud83d\udcc4 copier.yml\n\u251c\u2500\u2500 \ud83d\udcc4 macros      # the macros file\n\u251c\u2500\u2500 \ud83d\udcc4 context     # the context file\n\u251c\u2500\u2500 \ud83d\udcc1 extensions\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 \ud83d\udcc4 slugify.py\n\u2514\u2500\u2500 \ud83d\udcc1 {{project_name|slugify}}\n    \u2502\n    \u2502   # using the macros\n    \u251c\u2500\u2500 \ud83d\udcc4 {% import 'macros' as macros with context %}{{macros.module_name()}}.py.jinja\n    \u2502\n    \u2502   # or using the enhanced context\n    \u2514\u2500\u2500 \ud83d\udcc4 {% from 'context' import ctx with context %}{{ctx.module_name}}.py.jinja\n```\n\nAs you can see, both forms are really ugly to write:\n\n- the `macros` or `context` can only be placed in the root,\n  as slashes `/` are not allowed in filenames\n- you must use spaces and single-quotes\n  (double-quotes are not valid filename characters on Windows)\n  in your templated filenames, which is not clean\n- filenames are very long\n\n**Using our context hook instead makes it so easy and clean!**\n\n```python\nfrom copier_template_extensions import ContextHook\n\n\nclass ContextUpdater(ContextHook):\n    def hook(self, context):\n        context[\"module_name\"] = \"app\" if context[\"project_type\"] == \"webapi\" else \"cli\"\n```\n\n```\n\ud83d\udcc1 template_root\n\u251c\u2500\u2500 \ud83d\udcc4 copier.yml\n\u251c\u2500\u2500 \ud83d\udcc1 extensions\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 \ud83d\udcc4 slugify.py\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 \ud83d\udcc4 context.py\n\u2514\u2500\u2500 \ud83d\udcc1 {{project_name|slugify}}\n    \u2514\u2500\u2500 \ud83d\udcc4 {{module_name}}.py.jinja\n```\n\nYou can do many more things with a context hook,\nlike downloading data from external resources\nto include it in the context, etc.\n\n> [!TIP]\n> **Context hooks run during every Copier rendering phase.**\n> During project generation or project updates, Copier passes\n> through several rendering phases: when prompting (questions / answers),\n> when rendering files, when running tasks, and when running migrations.\n>\n> By default, a context hook runs during all these phases,\n> possibly multiple times, for example once per prompted question,\n> or once per rendered file. The task of running the hook only once,\n> or during a specific phase only, is left to you.\n>\n> To run only once, you can use caching within your class\n> (for example by storing computed values in class variables).\n>\n> To run during a specific phase only, you can check the value\n> of `context[\"_copier_phase\"]` (Copier 9.6+), which is one of:\n> `\"prompt\"`, `\"render\"`, `\"tasks\"`, `\"migrate\"`.\n>\n> Other key-value pairs can be found in the context\n> that you might find useful (Copier configuration, etc.).\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Special Jinja2 extension for Copier that allows to load extensions using file paths relative to the template root instead of Python dotted paths.",
    "version": "0.3.3",
    "project_urls": {
        "Changelog": "https://github.com/copier-org/copier-template-extensions/blob/main/CHANGELOG.md",
        "Discussions": "https://github.com/copier-org/copier-template-extensions/discussions",
        "Documentation": "https://github.com/copier-org/copier-template-extensions",
        "Funding": "https://github.com/sponsors/pawamoy",
        "Homepage": "https://github.com/copier-org/copier-template-extensions",
        "Issues": "https://github.com/copier-org/copier-template-extensions/issues",
        "Repository": "https://github.com/copier-org/copier-template-extensions"
    },
    "split_keywords": [
        "copier",
        " templates",
        " extension"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "c6da117372469b9bb1555ae7f8a0b1f3e00345044e6006c50f3f6d3cb20c9843",
                "md5": "40ad12f8fcaf0ac059c0a5156a18111b",
                "sha256": "bf6bbdebada26132640d6e1128723fa1ff7eb59d008de07a7543c51e0facb0ea"
            },
            "downloads": -1,
            "filename": "copier_template_extensions-0.3.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "40ad12f8fcaf0ac059c0a5156a18111b",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 9893,
            "upload_time": "2025-07-15T10:40:39",
            "upload_time_iso_8601": "2025-07-15T10:40:39.928937Z",
            "url": "https://files.pythonhosted.org/packages/c6/da/117372469b9bb1555ae7f8a0b1f3e00345044e6006c50f3f6d3cb20c9843/copier_template_extensions-0.3.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8a471a6638b4323a9eba2f19f4b1364bd77fcdb2b525a1c1cccb1f73422ff97e",
                "md5": "3928d4997d03c9bf2deb8137d6f6cc8c",
                "sha256": "86bd5e2e89c94b6f9321e4078781b4456c5658b820bcae49ac60fd1e1dd23c10"
            },
            "downloads": -1,
            "filename": "copier_template_extensions-0.3.3.tar.gz",
            "has_sig": false,
            "md5_digest": "3928d4997d03c9bf2deb8137d6f6cc8c",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 34991,
            "upload_time": "2025-07-15T10:40:41",
            "upload_time_iso_8601": "2025-07-15T10:40:41.309276Z",
            "url": "https://files.pythonhosted.org/packages/8a/47/1a6638b4323a9eba2f19f4b1364bd77fcdb2b525a1c1cccb1f73422ff97e/copier_template_extensions-0.3.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-15 10:40:41",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "copier-org",
    "github_project": "copier-template-extensions",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "copier-template-extensions"
}
        
Elapsed time: 2.68422s