Jinja2 CherryPy Tool for Easy Templating

  • By Aaron
  • Mon 10 December 2018
  • Updated on Sat 15 December 2018

As I dig deeper into CherryPy, I wanted to take full advantage of the framework by writing a templating tool.

My previous solution was to create a Page() class which was extended by Root() and had a method called build_page(). In my latest CherryPy project, I want to get rid of the need for the Page() class altogether and replace it with cherrypy.tools.

In this section of the documentation, we are led to an outdated method of implementing a Jinja2 templating tool. In the source code, we see the following:

cherrypy.response.body = template.render(**data)

When I tried using this method in my own tool, it wasn't working. The cherrypy.response.body object is a type of generator that chunks the output to the browser. Overwriting this data with a string was causing issues.

CherryPy comes with a JSON output tool by default. The nature of JSON is to accept arbitrary data structures and output them as a formatted string. So it was at least technically possible for a tool to process a dict as the return value of a handler. This dict can then be passed to the Jinja2 rendering method.

In order to accomplish this, the JSON output tool uses the 'before_handle' hook to replace the handler in cherrypy.serving.request.handler. It then made a 'backup' of the original handler, and, when called, the JSON handler executes the original page handler itself, allowing it to receive the raw output of that method, in our case a dictionary.

The main difference between the json_out tool and our Jinja2 tool is that we need a stateful class in order to initialize our Jinja2 environment, i.e. a class instead of a function.

Our templating tool can be called like this:

@cherrypy.expose
@cherrypy.tools.render(template='mypage.html')
def mypage(self, foo, bar):
    return {'foo': foo, 'bar': bar}

And the code behind this decorator...

# -*- coding: utf-8 -*-

import cherrypy

from jinja2 import Environment, FileSystemLoader

class Jinja2Tool(cherrypy.Tool):

    def __init__(self, template_path):

        cherrypy.Tool.__init__(self, 'before_handler', self.set_handler, priority=5)

        # Converts all None types to empty strings during output
        self.env = Environment(
                       loader=FileSystemLoader(template_path),
                       finalize=self.finalize
                   )

    def finalize(self, x):
        return '' if x is None else x

    def set_handler(self, template=None):

        self.template = template

        request = cherrypy.serving.request

        if request.handler is None:
            return

        request._render_inner_handler = request.handler
        request.handler = self.render

    def render(self, *args, **kwargs):

        # Decorator not applied, return original handler
        if self.template is None:
            return cherrypy.serving.request._render_inner_handler(*args, **kwargs)

        template = self.env.get_template(self.template)
        value = cherrypy.serving.request._render_inner_handler(*args, **kwargs)

        return template.render(**value)

Enable the tool in your config file... (actually, this isn't true, don't do this).

[/]
tools.render.on: True

If you turn this tool on in the config file, it will execute for every page request instead of just the handlers you've decorated with the Tool you've made.

Now all you have to do is initialize the tool in your serving script and you're all set!

cherrypy.tools.render = Jinja2Tool(conf['path']['templates'])

This tool can be found in src/tools/jinja2tool.py as part of my CherryPy pet project, CherryEx. CherryEx aims to add some convenience mechanisms for building projects, but also aims to completely decouple the HTTP framework from the actual application code. The goal of this is to allow CherryEx to evolve as a convenient web framework that can be easily plugged in to any project I am working on. This way the project source can be an application in its own right (with its own repo) instead of strictly a CherryPy app.

Happy coding!