3

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!

AlexVhr
  • 2,014
  • 1
  • 20
  • 30

4 Answers4

4

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
larsks
  • 277,717
  • 41
  • 399
  • 399
  • Yes, that's much better but still too cumbersome. That {{invoice|nested_dict('', 'customer.address.city')}}''') is an instant no-go - the template will become unreadable. – AlexVhr Mar 17 '15 at 10:26
  • 1
    I'll admit that I don't see much of a difference in readability between this one and what you've proposed, but I'm glad you've found a solution that works for you. – larsks Mar 17 '15 at 12:06
  • You are right, my solution is not that much more readable then yours, but it has another advantage - it handles callables. With arguments. Which is a good thing :) – AlexVhr Mar 17 '15 at 12:12
2

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
Vor
  • 33,215
  • 43
  • 135
  • 193
  • You are proposing to write a custom filter for each case? A simple invoice template would require a dozen of them! – AlexVhr Mar 17 '15 at 10:15
2

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.

Community
  • 1
  • 1
AlexVhr
  • 2,014
  • 1
  • 20
  • 30
0

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
    }
})

jde-chil
  • 112
  • 2
  • 4