9

I am trying to get all undefined variables from a Jinja2 template. Assume that I have a template like below.

tmpstr = """
{% for row in csv %}
sample {{row.field1}} stuff {{row.field2}} morestuff {{row.field3}}
{% endfor %}
"""

and input dictionary as below

cxt = {'csv': [
    {'field3': 1234, 'field4': 12314},
    {'field3': 2222, 'field4': 1213}
]}

Here is how I try to render it.

env = Environment(undefined=Undefined)
tmp = env.from_string(tmpstr)
tmpsrc = tmp.render(cxt)
print(tmpsrc)

Template expect variables field1, field2 and field3 to be present. However field1 and field2 are not present. My aim is to find all missing variables.

Jinja2 silently ignores missing variables. Therefore I tried to add StrictUndefined option:

errs = []
try:
    env = Environment(undefined=StrictUndefined)
    tmp = env.from_string(tmpstr)
    tmpsrc = tmp.render(cxt)
except Exception as e:
    errs.append(str(e))
print(errs)

However this time jinja2 complains about only the first missing variable which is field1.

Therefore I tried another option which is DebugUndefined. This option does not raise an exception and leaves missing variables placeholder in the template output untouched. Thus I can not collect missing variables.

Can you please suggest how can I get missing variables in a jinja2 template?

Here is runnable code if anyone wants to try it out:

from jinja2 import BaseLoader,Environment,StrictUndefined,DebugUndefined,Undefined
tmpstr = """
{% for row in csv %}
sample {{row.field1}} stuff {{row.field2}} morestuff {{row.field3}}
{% endfor %}
"""
cxt = {'csv': [
    {'field3': 1234, 'field4': 12314},
    {'field3': 2222, 'field4': 1213}
]}
env = Environment(undefined=Undefined)
tmp = env.from_string(tmpstr)
tmpsrc = tmp.render(cxt)
print('CASE 1: undefined=Undefined')
print(tmpsrc)

errs = []
try:
    env = Environment(undefined=StrictUndefined)
    tmp = env.from_string(tmpstr)
    tmpsrc = tmp.render(cxt)
except Exception as e:
    errs.append(str(e))
print('CASE 2: undefined=StrictUndefined')
print(errs)

errs = []
try:
    env = Environment(undefined=DebugUndefined)
    tmp = env.from_string(tmpstr)
    tmpsrc = tmp.render(cxt)
except Exception as e:
    errs.append(str(e))

print('CASE 3: undefined=DebugUndefined')
print(errs)
print(tmpsrc)
Gustavo Bezerra
  • 9,984
  • 4
  • 40
  • 48
moth
  • 427
  • 1
  • 4
  • 18
  • It is possible to get required variables using **jinja2schema** as an alternative. – moth Apr 17 '19 at 15:00
  • [jinja2 - How to get list of all variables in jinja 2 templates - Stack Overflow](https://stackoverflow.com/questions/8260490/how-to-get-list-of-all-variables-in-jinja-2-templates/26319381#26319381) – user202729 May 31 '19 at 08:38

5 Answers5

11

I found the solution for your question, using jinja2.make_logging_undefined. I was in the same boat as you are and have been search high and low for an answer. Most of the answer pointing me to use parsed templates, however I couldn't figure out how to get the context into the parsed templates.

I finally able to make this work using the make_logging_undefined. If you want to find all the undefined variables, make sure to use just the Undefined base class rather than StrictUndefined. Using StrictUndefined will cause jinja to throw exception at the first encounter of the undefine.

Just a disclaimer: I'm not an python nor jinja expert, so the code is not the most efficient nor structured. But it serves my purpose. This is just POC code.

Here's the code:

import jinja2
import logging
from jinja2 import Environment, Undefined
from jinja2.exceptions import UndefinedError

def main():
    templateLoader = jinja2.FileSystemLoader( searchpath="D:\\somelocation\\" )

    logging.basicConfig()
    logger = logging.getLogger('logger')
    LoggingUndefined = jinja2.make_logging_undefined(logger=logger,base=jinja2.Undefined)

    templateEnv = jinja2.Environment( loader=templateLoader, undefined=LoggingUndefined)

    TEMPLATE_FILE = "./example1.jinja"

    template = templateEnv.get_template( TEMPLATE_FILE )

    FAVORITES = [ "chocolates", "lunar eclipses", "rabbits" ]
    # Specify any input variables to the template as a dictionary.
    templateVars = { "title" : "Test Example",
                     "description" : "A simple inquiry of function.",
                     "favorites" : FAVORITES,
                     "whatever" : "1"
                   }    
    # Finally, process the template to produce our final text.
    try:
        outputText = template.render( templateVars )
    except ( UndefinedError) as err:
        print err

if __name__ == '__main__':
    main()

example1.jinja:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />

  <title>{{ title }}</title>
  <meta name="description" content="{{ description }}" />
</head>

<body>

<div id="content">
  <p>Greetings visitor!  These are a list of my favorite things:</p>

  <ul>
  {% for item in favorites %}
    <li>{{ item }}</li>

  <li>My favorites: {{ favorites[1] }} </li>
  {% endfor %}
  {{ undefined_var1 }}
  {{ underfined_var2 }}
  </ul>
</div>

</body>
</html>

Here's the sample output:

WARNING:logger:Template variable warning: undefined_var1 is undefined
WARNING:logger:Template variable warning: underfined_var2 is undefined
Balitong
  • 111
  • 4
  • i have finally used **DebugUndefined** and used regex `{{(.*?)}}` to find missing variables in rendered template. however i will upvote for your answer because it makes use of logging. thanks. – moth Nov 25 '17 at 15:14
  • Thanks for the upvote. The output using the make_logging_undefined are all the none defined variables in the example. Here's the sample output : `WARNING:logger:Template variable warning: undefined_var1 is undefined` `WARNING:logger:Template variable warning: underfined_var2 is undefined` – Balitong Nov 27 '17 at 07:57
8

Using find_undeclared_variables with DebugUndefined you can properly raise an exception mentioning all variables that are missing:

import jinja2
from jinja2.meta import find_undeclared_variables

env = jinja2.Environment(undefined=jinja2.DebugUndefined)
template = env.from_string('foo={{ foo }}, bar={{ bar}}, baz={{ baz }}')

# Render template without passing all variables
rendered = template.render(foo=1)

# Check if rendering was done correctly
ast = env.parse(rendered)
undefined = find_undeclared_variables(ast)  # {'bar', 'baz'}
if undefined:
    raise jinja2.UndefinedError(f'The following variables are undefined: {undefined!r}')

If you prefer logging, you can replace the exception raising with your own logging calls using the contents of undefined.

PS: I am relatively new to Jinja, but I am quite surprised this is not the default behavior of env.render. I wonder why authors/maintainers think having missing variables silently ignored by default being a good thing...

Gustavo Bezerra
  • 9,984
  • 4
  • 40
  • 48
  • 5
    if you use a dictionary items in the template and have missing variables then parsing will fail. Here is the example ``` import jinja2 from jinja2.meta import find_undeclared_variables env = jinja2.Environment(undefined=jinja2.DebugUndefined) template = env.from_string('foo={{ foo }}, bar={{ bar}}, baz.prop1 ={{ baz.prop1 }} baz.prop2 ={{ baz.prop2 }}') rendered = template.render(foo=1,baz={'prop1':'prop1','prop3':'prop2'}) ast = env.parse(rendered) ``` – moth Apr 17 '19 at 14:47
1

You can simply create your own "Undefined" so you can e.g. process the list of undefined variables programmatically. Here's an example:


missing_vars=[]
class CollectingUndefined(jinja2.Undefined):

    def _add_missing_var(self):
        missing_vars.append(self._undefined_name)

    def __iter__(self):
        self._add_missing_var()
        return super().__iter__();

    def __str__(self):
        self._add_missing_var()
        return super().__str__();

    def __len__(self):
        self._add_missing_var()
        return super().__len__();

    def __eq__(self):
        self._add_missing_var()
        return super().__eq__();

    def __ne__(self):
        self._add_missing_var()
        return super().__eq__();

    def __bool__(self):
        self._add_missing_var()
        return super().__e__bool__q__();

    def __hash__(self):
        self._add_missing_var()
        return super().__hash__();
Michael Wyraz
  • 3,638
  • 1
  • 27
  • 25
1

Concerning your first attempt (reposted here)

errs = []
try:
    env = Environment(undefined=StrictUndefined)
    tmp = env.from_string(tmpstr)
    tmpsrc = tmp.render(cxt)
except Exception as e:
    errs.append(str(e))
print(errs)

I believe the problem is that 1) You're trying to loop in the template when you should try to loop in the script and 2) you aren't updating the cxt after each exception.

I needed to do the same thing with a template using custom delimiters ( for which find_undeclared_variables won't work)

and I used something like this:

def findAllUndefined(target):
    jinja_env = jinja2.Environment(undefined=jinja2.StrictUndefined)
    doc = DocxTemplate(target)
    context = {}
    finished = False
    while finished == False:
        try:
            doc.render(context, jinja_env)
            finished = True
        except Exception as e:
            tag = re.sub(" is undefined", "", str(e)) # extracting tag name from error message
            tag = re.sub("'", "", tag)
            context[str(tag)] = "FOUND"
    return context.keys()

The idea being that every time an undefined variable is encountered, the tag name is inserted into the context with a fluff value, and the rendering is attempted again until all variables are known and catalogued.

0

A good way to achieve that in my opinion is to define your own Undefined class, similarly to Michael Wyraz's anwer:

class CollectUndefined(object):
    def __init__(self, undefined_cls=Undefined):
        self.undefined_cls = undefined_cls
        self.missing_vars = []

    def __call__(self, *args, **kwds):
        undefined = self.undefined_cls(*args, **kwds)
        self.missing_vars.append(undefined._undefined_name)
        return undefined

    def assert_no_missing_vars(self):
        if len(self.missing_vars) > 0:
            raise MissingVariablesError(self.missing_vars)


class MissingVariablesError(Exception):
    def __init__(self, missing_vars, *args):
        super().__init__(*args)
        self.missing_vars = missing_vars

    def __str__(self):
        return 'Missing variables: {}'.format(self.missing_vars)

Then you can use it like so:

env = Environment(undefined=CollectUndefind())
tmp = env.from_string(tmpstr)
tmpsrc = tmp.render(cxt)
print(env.undefined.missing_vars)

Or with try-except:

env = Environment(undefined=CollectUndefind())
tmp = env.from_string(tmpstr)
try:
    tmpsrc = tmp.render(cxt)
    env.undefined.assert_no_missing_vars():
except MissingVariablesError as e:
    print(e.missing_vars)
    
yahelck
  • 41
  • 5