Integrating Mako with Django 1.8 template backend subsystem

Idego Idego • Feb 09
Post Img

Django 1.8 release is coming soon, and one of the new important features is
support for multiple template engines. Allowing to choose between template engines is mostly caused by the need for greater performance in complex templates and a more convenient API for custom tags. It’s also interesting to note that the integrating Mako was funded with an indiegogo campaign. If you want to read more about the development process – head to Aymeric Augustin’s blog.

From the developers perspective 1.8 will change the way templates options are declared in settings (though of course old settings will remain compatible for some period of time). As for built-in backends DTL(Django template language) will still be the default, and Jinja2 support will also be an official part of the new release.

In this article, I’ll show you how to write your own template backend – since
Jinja2 is already there, I’ve chosen another popular engine – Mako.

Writing the custom backend

According to Django docs, writing a custom backend should be very easy, We need one class inheriting BaseEngine that will return Template objects for us – those can be again our custom classes, as long as they have a render method, accepting context and request parameters.

Django source code and docs even contain example code for that, let’s go
through it, as there are some parts that may seem odd:


from django.template import TemplateDoesNotExist, TemplateSyntaxError  
from django.template.backends.base import BaseEngine  
from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy

import foobar


class FooBar(BaseEngine):

    # Name of the subdirectory containing the templates for this engine
    # inside an installed application.
    app_dirname = 'foobar'

    def __init__(self, params):
        params = params.copy()
        options = params.pop('OPTIONS').copy()
        super(FooBar, self).__init__(params)

        self.engine = foobar.Engine(**options)


    def from_string(self, template_code):
        try:
          return Template(self.engine.from_string(template_code))
        except foobar.TemplateCompilationFailed as exc:
            raise TemplateSyntaxError(exc.args)

    def get_template(self, template_name):
        try:
            return Template(self.engine.get_template(template_name))
        except foobar.TemplateNotFound as exc:
            raise TemplateDoesNotExist(exc.args)
        except foobar.TemplateCompilationFailed as exc:
            raise TemplateSyntaxError(exc.args)


class Template(object):

    def __init__(self, template):
        self.template = template

    def render(self, context=None, request=None):
        if context is None:
            context = {}
        if request is not None:
            context['request'] = request
            context['csrf_input'] = csrf_input_lazy(request)
            context['csrf_token'] = csrf_token_lazy(request)
        return self.template.render(context)

In order to use the backend you should also modify your settings file, like this:


TEMPLATES = [  
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.template.context_processors.tz',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
    {
        'BACKEND': 'mako_template_app.mako_template_backend.MakoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.template.context_processors.tz',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        }
    }
]

The idea is very simple:

  • implement a class that inherits from BaseEngine
  • implements from_string
  • implement get_template
  • they should return an object that has a render(self, context=None, request=None) signature

I’ve actually tried doing that, and the first thing I got was an error
screen with “A server error occurred. Please contact the administrator.”
message – not the usual Django detailed error page, only this one sentence. The background runserver process also spit out an enormous stack trace. I started digging, and making my code look more and more like the example one. Finally, the lines that helped were:


params = params.copy()  
options = params.pop('OPTIONS').copy()  

And the lines that were causing the error were in the BaseEngine init implementation:


    def __init__(self, params):
        params = params.copy()
        self.name = params.pop('NAME')
        self.dirs = list(params.pop('DIRS'))
        self.app_dirs = bool(params.pop('APP_DIRS'))
        if params:
            raise ImproperlyConfigured(
                "Unknown parameters: {}".format(", ".join(params)))

So – my settings.py specified some options for the backend, but since the initial version of my __init__ did not clear them, there was the exception, but since the template backends were not set up, I could not see the detailed error message in the browser.

With that fixed, the rest of the code was relatively easy to write – at least errors messages were more comprehensive. One more thing stood out of the example for me – the render method of the template object, which looks like this:


def render(self, context=None, request=None):
if context is None:
context = {}
if request is not None:
context['request'] = request
context['csrf_input'] = csrf_input_lazy(request)
context['csrf_token'] = csrf_token_lazy(request)
return self.template.render(context)

What’s the reason for the contextmanipulation here, shouldn’t this be done somewhere else – like in the contextprocessors I’ve specified?. Well, it turns out, that only the Django template engine supports contextprocessors – even built-in Jinja2 does not have support them. This is kind of unfortunate – especially, since contextprocessors are just functions that take request as input, and return a dictionary of necessary context updates as output (side note: django mailing list has a discussion about whether context processors are needed at all – read about it here). Equipped with that knowledge I’ve started my second take on incorporating Mako. This is the result:


class CustomTemplateBackend(BaseEngine):  
    def __init__(self, params):
        params = params.copy()
        options = params.pop('OPTIONS').copy()
        super(CustomTemplateBackend, self).__init__(params)
        self.context_processors = []
        if 'context_processors' in options:
            self.context_processors = options.pop('context_processors')
        self.init_engine(options)

    @cached_property
    def template_context_processors(self):
        context_processors = _builtin_context_processors
        context_processors += tuple(self.context_processors)
        return tuple(import_string(path) for path in context_processors)


class MakoTemplates(CustomTemplateBackend):  
    app_dirname = 'mako'

    def init_engine(self, options):
        self.lookup = TemplateLookup(directories=self.template_dirs, **options)

    def from_string(self, template_code):
        try:
            return MakoTemplateWrapper(self, Template(template_code))
        except SyntaxException as exc:
            six.raise_from(TemplateSyntaxError, exc)

    def get_template(self, template_name):
        try:
            return MakoTemplateWrapper(self, self.lookup.get_template(template_name))
        except TemplateLookupException as exc:
            six.raise_from(TemplateDoesNotExist, exc)
        except SyntaxException as exc:
            six.raise_from(TemplateSyntaxError, exc)


class ContextProcessorTemplate(object):  
    def __init__(self, engine, template):
        self.engine = engine
        self.template = template

    def render(self, context=None, request=None):
        if context is None:
            context = {}
        for processor in self.engine.template_context_processors:
            context.update(processor(request))
        return self.render_internal(context)


class MakoTemplateWrapper(ContextProcessorTemplate):  
    def render_internal(self, context):
        return self.template.render(**context)

Let’s go through important modifications:

  • First of all, I’ve extracted the quirks of BaseEngine inheritance into CustomTemplateBackend. This class can be reused if I ever decide to write backends for other templating engines, and I won’t have to worry about popping OPTIONS again.
  • The second change involves using templatecontextprocessors – This code is mostly taken from Django source code – the Engine class (used internally by DjangoEngine which extends BaseEngine) and RequestContext. With that, I can support existing context processors and don’t have to remember about quirks in my template wrapper objects. This class can also be reused by other backends.
  • MakoTemplates initializes TemplateLookup (which is roughly equivalent of Jinja2 environment, with any options provided in the settings) – so user have control over mako initialization – for example, they can set the filter_exception flag to true, to get a better insight into why template rendering failed. An alternative would be to let users specify a custom callable that would return Lookup (in a similar way Jinja2 backend environment setting work)
  • from_string and get_template translate mako specific exceptions into django specific ones – to keep code compatible with both python 3, and python 2 we use six.raise_from
  • app_dirname specifies the default directory to look for templates, for DTL this is the familiar ‘templates’, for other backends, it’s best to use an unique name. Additional directories can be also set in settings.

Referencing the templates from your view functions:

This is actually quite easy – the render call does not change


def hello(request):  
    return render(request, 'mako_template_app/hello.mako.html', {})

This has one caveat though – If you have several template backend defined (and chances are, you have – for example you want to use Jinja2 for your templates and Django for admin pages) – each of them is being scanned for the template, which can slightly reduce your performance. As for now, the render method does not provide a way to select the template backend, but django.template.loader exposes methods with using a parameter with which you can explicitly specify the desired backend and skip the exhaustive search.

Software Engineers - development team

Wrap-up

Although currently there are some quirks in the API (at the moment of writing this article Django 1.8 was at the alpha stage), the support for multiple template engines in Django looks promising and is not difficult to use – and once the release date approaches packages for integrating most popular engines will most likely be already available.

Hope you enjoyed the read, if we didn’t dispel your doubts please let us know. We’d love to help you – click here!


Leave a Reply

Your email address will not be published. Required fields are marked *

Drive tactical delivery
without inflating the top line

Your Swiss Army Knife in AI, Cloud and Digital

Get in touch Button Arrow