Reputation: 2074
I've got this:
template = '{{invoice.customer.address.city}}'
And it works fine. But sometimes invoice.customer is Null or invoice.customer.address is Null
and then jinja throws jinja2.exceptions.UndefinedError: 'None' has no attribute 'address'
because it can't reach that .city
part. So how do I tell it to just fail silently if it can't access an attribute?
Thanks!
Upvotes: 3
Views: 7415
Reputation: 132
It needs further testing as it might break things, but what about extending the
Environment
class and override the gettatr (or getitem) method like this
from jinja2 import Environment
class SEnvironment(Environment):
ERROR_STRING = 'my_error_string'
def getattr(self, obj, attribute):
"""Get an item or attribute of an object but prefer the attribute.
Unlike :meth:`getitem` the attribute *must* be a bytestring.
"""
try:
return getattr(obj, attribute)
except AttributeError:
pass
try:
return obj[attribute]
except (TypeError, LookupError, AttributeError):
return SEnvironment.ERROR_STRING # this lines changes
then if you want to handle errors you can create filters like raise_error
or dislay_error
def raise_error(obj):
if obj == SEnvironment.ERROR_STRING:
raise Exception('an error occured')
return obj
def print_error(obj, _str='other error'):
if obj == SEnvironment.ERROR_STRING:
return _str
return obj
jinja_env = SEnvironment()
jinja_env.filters['raise_error'] = raise_error
jinja_env.filters['print_error'] = print_error
jinja_env = jinja_env.from_string("""{{ test1.test2.test3 }}""") # -> my_error_string
#jinja_env = jinja_env.from_string("""{{ test1.test2.test3|print_error('<none>') }}""") # -> <none>
#jinja_env = jinja_env.from_string("""{{ test1.test2.test3|raise_error }}""") # -> Exception: an error occured
res = jinja_env.render({
'test1': {
'test2': None
}
})
Upvotes: 0
Reputation: 2074
Ok, I think I got it. The answer seems to be in using globals, like it is described here
So I've tried to build on that, and the result was this:
def jinja_global_eval(c, expr):
"""Evaluates an expression. Param c is data context"""
try:
return str(eval(expr))
except:
return ''
After installing this into my template environment with templating_env.globals['eval'] = jinja_global_eval
I now can do this in my templates:
{{eval(invoice, 'c.customer.address.city')}}
and this:
{{eval(invoice, 'c.customer.get_current_balance()')}}
It will probably bite my pants during debugging, but to avoid it a simple logging could be installed into jinja_global_eval
. Anyways, thanks to all who tried to help.
Upvotes: 2
Reputation: 312293
If you are doing this frequently, rather than creating a per-attribute filter you could generalize Vor's answer to work for arbitrary nested dictionaries, like this:
import jinja2
def filter_nested_dict(value, default, path):
keys = path.split('.')
for key in keys:
try:
value = value[key]
except KeyError:
return default
return value
env = jinja2.Environment()
env.filters['nested_dict'] = filter_nested_dict
template = env.from_string('''
City: {{invoice|nested_dict('<none>', 'customer.address.city')}}''')
Given the above, this:
print template.render(invoice={})
Gives you:
City: <none>
And this:
print template.render(invoice={'customer': {'address': {'city': 'boston'}}})
Gives you:
City: boston
Upvotes: 4
Reputation: 35149
I would suggest you to create a custom filter and pass the whole invoice
object to it rather then trying to find workarounds in Jinja.
For example:
import jinja2
def get_city_from_invoice(invoice):
try:
return invoice['customer']['address']['city']
except KeyError:
return None
env = jinja2.Environment()
env.filters['get_city_from_invoice'] = get_city_from_invoice
d = {'invoice': {'customer': {'address': {'city': 'foo'}}}}
d1 = {'invoice': {'no-customers': 1 }}
print "d: ", env.from_string('{{ invoice | get_city_from_invoice }}').render(d)
print "d1: ", env.from_string('{{ invoice | get_city_from_invoice }}').render(d1)
Will print:
d: foo
d1: None
Upvotes: 2