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!