AlexVhr
AlexVhr

Reputation: 2074

How to handle attribute access errors in Jinja2?

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

Answers (4)

jde-chil
jde-chil

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

AlexVhr
AlexVhr

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

larsks
larsks

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

Vor
Vor

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

Related Questions