Reputation: 16860
I want to render a Jinja2 Template using a custom object implementing the __getitem__
interface. The object implements a lazy variable look up because it is impossible to create a dictionary from it (the number of available variables is almost infinite, value retrieval works dynamically on the queried key).
Is it possible to render a Jinja2 template using a context object?
# Invalid code, but I'd like to have such an interface.
#
from jinja2 import Template
class Context(object):
def __getitem__(self, name):
# Create a value dynamically based on `name`
if name.startswith('customer'):
key = name[len('customer_'):]
return getattr(get_customer(), key)
raise KeyError(name)
t = Template('Dear {{ customer_first }},\n')
t.render(Context())
Upvotes: 5
Views: 3224
Reputation: 25134
After a ton of digging I found out the cleanest way to do this.
First create a subclass of jinja2.runtime.Context
that implements resolve_or_missing
(docs):
from jinja2.runtime import Context
class MyContext(Context):
"""A custom jinja2 context class."""
def resolve_or_missing(self, key):
# TODO(you): Add your custom behavior here
return super(TrackingContext, self).resolve_or_missing(key)
Then all you need to do is set the context_class
variable of the Jinja Environment (docs)
env.context_class = MyContext
I was using Flask so I did this:
flask.current_app.jinja_environment.context_class = MyContext
Upvotes: 1
Reputation: 5874
It looks like you have a function, get_customer()
which returns a dictionary, or is an object?
Why not just pass that to the template?
from jinja2 import Template
t = Template('Dear {{ customer.first }},\n')
t.render(customer=get_customer())
IIRC, Jinja is pretty forgiving of keys that don't exist, so customer.bogus_key
shouldn't crash.
Upvotes: 1
Reputation: 16860
I now figured out this (extremely hacky and ugly) solution.
t = CustomTemplate(source)
t.set_custom_context(Context())
print t.render()
Using the following replacements:
from jinja2.environment import Template as JinjaTemplate
from jinja2.runtime import Context as JinjaContext
class CustomContextWrapper(JinjaContext):
def __init__(self, *args, **kwargs):
super(CustomContextWrapper, self).__init__(*args, **kwargs)
self.__custom_context = None
def set_custom_context(self, custom_context):
if not hasattr(custom_context, '__getitem__'):
raise TypeError('custom context object must implement __getitem__()')
self.__custom_context = custom_context
# JinjaContext overrides
def resolve(self, key):
if self.__custom_context:
try:
return self.__custom_context[key]
except KeyError:
pass
return super(CustomContextWrapper, self).resolve(key)
class CustomTemplate(JinjaTemplate):
def set_custom_context(self, custom_context):
self.__custom_context = custom_context
# From jinja2.environment (2.7), modified
def new_context(self, vars=None, shared=False, locals=None,
context_class=CustomContextWrapper):
context = new_context(self.environment, self.name, self.blocks,
vars, shared, self.globals, locals,
context_class=context_class)
context.set_custom_context(self.__custom_context)
return context
# From jinja2.runtime (2.7), modified
def new_context(environment, template_name, blocks, vars=None,
shared=None, globals=None, locals=None,
context_class=CustomContextWrapper):
"""Internal helper to for context creation."""
if vars is None:
vars = {}
if shared:
parent = vars
else:
parent = dict(globals or (), **vars)
if locals:
# if the parent is shared a copy should be created because
# we don't want to modify the dict passed
if shared:
parent = dict(parent)
for key, value in iteritems(locals):
if key[:2] == 'l_' and value is not missing:
parent[key[2:]] = value
return context_class(environment, parent, template_name, blocks)
Can anyone offer a better solution?
Upvotes: 3