如何从Jinja2模板中获取所有未定义的变量?

10

我正在尝试从Jinja2模板中获取所有未定义的变量。 假设我有以下模板。

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(tmpsrc)

模板期望变量field1field2field3存在。但是field1field2不存在。我的目标是找到所有缺失的变量。

Jinja2会默默地忽略缺失的变量。因此我尝试添加StrictUndefined选项:

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)

然而这次jinja2抱怨只有第一个缺失变量`field1`。因此我尝试了另一种选项,即`DebugUndefined`。这个选项不会引发异常并且将模板输出中缺失的变量占位符保持不变。因此我无法收集缺失的变量。请问如何在jinja2模板中获取缺失的变量?如果想要运行代码,请查看以下可运行代码:
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)

可以使用 jinja2schema 作为替代方案来获取所需的变量。 - moth
6个回答

11

我找到了你问题的解决方案,使用 jinja2.make_logging_undefined。我曾经和你有同样的困扰并一直寻找答案。大部分答案都告诉我要使用解析模板,但我无法弄清楚如何将上下文传递到解析模板中。

最终我通过使用 make_logging_undefined 得以实现。如果您想查找所有未定义的变量,请确保仅使用 Undefined 基类而不是 StrictUndefined。使用 StrictUndefined 将导致 jinja 在第一次遇到未定义变量时抛出异常。

免责声明:我不是 Python 或 Jinja 专家,所以代码可能不是最有效或结构化的。但它能够满足我的需求。这只是 POC 代码。

以下是代码:

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

示例1.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>

这是示例输出:

WARNING:logger:Template variable warning: undefined_var1 is undefined
WARNING:logger:Template variable warning: underfined_var2 is undefined

我最终使用了 DebugUndefined 并使用正则表达式 {{(.*?)}} 在渲染的模板中查找缺失的变量。不过,我会为你的答案点赞,因为它利用了日志记录。谢谢。 - moth
感谢您的点赞。使用make_logging_undefined的输出是示例中所有未定义变量。以下是示例输出: WARNING:logger:Template variable warning: undefined_var1 is undefined WARNING:logger:Template variable warning: underfined_var2 is undefined - Balitong

8
使用 DebugUndefinedfind_undeclared_variables,您可以适当地引发异常并提到所有缺失的变量:
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}')

如果您喜欢记录日志,您可以使用undefined的内容替换异常抛出,使用自己的日志调用。
PS:我对Jinja相对较新,但我很惊讶env.render不是默认行为。我想知道作者/维护者为什么认为默认情况下忽略缺少的变量是一件好事...

5
如果在模板中使用了字典的项并且缺少变量,则解析会失败。以下是示例: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

1
你可以创建自己的“未定义”变量,以便能够编程处理未定义变量列表。以下是一个示例:

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__();

1
关于您的第一次尝试(在此重新发布)
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)

我认为问题在于:1)您正在尝试在模板中循环,而应该尝试在脚本中循环;2)每次异常后您没有更新cxt。
我需要使用自定义定界符来处理模板(其中find_undeclared_variables无法工作),所以我使用了类似以下的代码:
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()

每当遇到未定义的变量时,将标签名称插入上下文中并赋予一个无关紧要的值,然后再次尝试渲染,直到所有变量都被识别和分类。

0
我认为实现这一点的好方法是定义自己的 Undefined 类,类似于Michael Wyraz的答案中的做法:
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)

然后你可以这样使用它:
env = Environment(undefined=CollectUndefind())
tmp = env.from_string(tmpstr)
tmpsrc = tmp.render(cxt)
print(env.undefined.missing_vars)

或者使用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)
    

0
这是一段简单的代码,经过Jinja2版本3.1.2的测试(灵感来自@yahelck的回答):
from jinja2 import Environment, Undefined


def create_collector():
    collected_variables = set()

    class CollectUndefined(Undefined):
        def __init__(self, name, parent=None):
            self.name = name
            self.parent = parent
            collected_variables.add(str(self))

        def __str__(self):
            if self.parent is not None:
                return f"{self.parent}.{self.name}"
            return self.name

        def __getattr__(self, name: str):
            return CollectUndefined(name, parent=self)

    return collected_variables, CollectUndefined


def find_all_vars(template_content):
    vars, undefined_cls = create_collector()
    env = Environment(undefined=undefined_cls)
    tpl = env.from_string(template_content)
    tpl.render({})  # empty so all variables are undefined

    return vars

这是一个使用它的示例:
template_content = "{{ foo.bar.buzz }} {% if x.y %}{{ a.b }}{% endif %}"
print(find_all_vars(template_content))

结果将是:
{'foo.bar.buzz', 'x.y', 'x', 'foo.bar', 'foo'}

更新: 如果你想要一个"嵌套"的输出,假设你已经安装了pyyaml,这里有一个稍微不同的版本,它将输出你需要填写的yaml文件。
from jinja2 import Environment, Undefined
import yaml


def create_collector():
    collected_variables = {}

    class CollectUndefined(Undefined):
        def __init__(self, name, parent=None):
            self.name = name
            self.parent = parent
            self.register()

        def __str__(self):
            if self.parent is not None:
                return f"{self.parent}.{self.name}"
            return self.name

        def register(self):
            d = (
                collected_variables
                if self.parent is None
                else self.parent.register()
            )
            d[self.name] = d.get(self.name, {})
            return d[self.name]

        def __getattr__(self, name: str):
            return CollectUndefined(name, parent=self)

    return collected_variables, CollectUndefined


def find_all_vars(template_content):
    vars, undefined_cls = create_collector()
    env = Environment(undefined=undefined_cls)
    tpl = env.from_string(template_content)
    tpl.render({})  # empty so all variables are undefined
    return yaml.dump(vars).replace("{}", "<TODO>")


template_content = "{{ foo.bar.buzz }} {% if x.y %}{{ a.b }}{% endif %}"
print(find_all_vars(template_content))

这个版本输出:
foo:
  bar:
    buzz: <TODO>
x:
  y: <TODO>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接