Jinja2 allowing macros to be extended, but providing a fallback implementation

Viewed 198

In a project that I'm working on I'd like to be able to declare some common macros which can conveniently be extended by a theme based on Jinja templates.

Here is a representative example of the way I have themes setup and structured:

$ tree
.
├── base
│   ├── common_macros.j2
│   └── example.j2
├── example.py
└── theme1
    └── common_macros.j2

With example.py looking like:

from pathlib import Path
import jinja2


if __name__ == '__main__':
    tmplt_root = Path(__file__).parent

    # Setup the base loader to be able to use both "example.j2" and "base/example.j2",
    # thereby allowing themes to implement "example.j2" by extending "base/example.j2".
    base_template_paths = [str(tmplt_root), str(tmplt_root / 'base')]

    env1 = jinja2.Environment(
        loader=jinja2.FileSystemLoader(base_template_paths),
    )

    env_themed = jinja2.Environment(
        loader=jinja2.FileSystemLoader(['theme1/'] + base_template_paths),
    )

    print('BASE')
    print('----------')
    print(env1.get_template('example.j2').render())

    print('Theme 1')
    print('----------')
    print(env_themed.get_template('example.j2').render())

base/example.j2:

{% from "common_macros.j2" import macro1 %}

{% block main -%}
    {{ macro1() }}
{%- endblock main %}

base/common_macros.j2:

{% macro macro1() -%}
Macro 1 base
{% endmacro %}

With this setup I'm able to implement an overriden macro in theme1/common_macros.j2 (and can even call the base implementation!):

{% import "base/common_macros.j2" as base %}

{% macro macro1() -%}
    Theme macro 1!
    {{ base.macro1() }}
{% endmacro %}

So far, so good!

$ python example.py
BASE
----------


Macro 1 base

Theme 1
----------


Theme macro 1!
    Macro 1 base

The problem comes when I add another macro to base/common_macros.j2 (and use it in example.j2) at which point the following error occurs:

$ python example.py
BASE
----------


Macro 1 base

    Macro 2 base

Theme 1
----------
Traceback (most recent call last):
  File "example.py", line 25, in <module>
    print(env_themed.get_template('example.j2').render())
  File "jinja2/environment.py", line 1090, in render
    self.environment.handle_exception()
  File "jinja2/environment.py", line 832, in handle_exception
    reraise(*rewrite_traceback_stack(source=source))
  File "jinja2/_compat.py", line 28, in reraise
    raise value.with_traceback(tb)
  File "base/example.j2", line 3, in top-level template code
    {% block main -%}
  File "base/example.j2", line 5, in block "main"
    {{ macro2() }}
jinja2.exceptions.UndefinedError: the template 'common_macros.j2' (imported on line 1 in 'example.j2') does not export the requested name 'macro2'

Clearly this is caused by the theme1/common_macros.j2 override not implementing all of the macros that are in the base.

My question: What can I do to make this work without needing to redefine all of the macros from base/common_macros.j2 in the theme1/common_macros.j2?

Approaches that I've tried and failed with so far:

  1. Import all macros from base/common_macros.j2 into theme1/common_macros.j2 (equivalent of from base import * in Python - remember, I don't want to write down all the macro names that exist in base in each theme, otherwise upgrades would get tedious!).

    Unfortunately this doesn't exist (Jinja templates, how to do an import of all macros to follow the DRY principle?)

  2. Include the base macros with {% include "base/common_macros.j2" %} in the theme (akin to extends for non-macro templates).

    Unfortunately only variables exposed explicitly in theme1/common_macros.j2 are visible (as per https://jinja.palletsprojects.com/en/2.10.x/templates/#import)

I'm more than happy to hear alternative ideas about how to do theming with Jinja if it leads to a solution to the problem and doesn't increase the complexity.

0 Answers
Related