Skip to main content

Static Template Analysis

New in version 1.2.0

Use the analyze() or analyze_async() methods of a Liquid Template to traverse its abstract syntax tree and report template tag, filter and variable usage.

Variables

The object returned from analyze() is an instance of TemplateAnalysis. Its variables property is a dictionary mapping template variable names to a list of two-tuples. Each tuple is the template name and line number where the variable was found.

from liquid import Template

template = Template("""\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}
{% for name in people %}
{{ forloop.index }} - {{ greeting }}, {{ name }}!
{% endfor %}
""")

analysis = template.analyze()
print(list(analysis.variables))

for name, location in analysis.variables.items():
for template_name, line_number in location:
print(f"'{name}' found in '{template_name}' on line {line_number}")
output
['people', 'forloop.index', 'greeting', 'name']
'people' found in '<string>' on line 2
'people' found in '<string>' on line 3
'forloop.index' found in '<string>' on line 4
'greeting' found in '<string>' on line 4
'name' found in '<string>' on line 4

New in version 1.6.0

Variable names - the keys of TemplateAnalysis.variables, and others - are a str subclass that includes a parts property, being a tuple representation of a variable's parts.

from liquid import Template

template = Template("{{ data.some[thing['foo.bar']] }}")

for var, location in template.analyze().variables.items():
for template_name, line_number in location:
print(f"{var.parts} found in '{template_name}' on line {line_number}")
output
('data', 'some', ('thing', 'foo.bar')) found in '<string>' on line 1
('thing', 'foo.bar') found in '<string>' on line 1

Global Variables

The global_variables property of TemplateAnalysis is similar to variables, but only includes those variables that are not in scope from previous assign or capture tags, or added to a block's scope by a block tag.

from liquid import Template

template = Template("""\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}
{% for name in people %}
{{ forloop.index }} - {{ greeting }}, {{ name }}!
{% endfor %}
""")

analysis = template.analyze()
print("all variables: ", list(analysis.variables))
print("global variables: ", list(analysis.global_variables))
output
all variables:  ['people', 'forloop.index', 'greeting', 'name']
global variables: ['greeting']

While greeting is assumed to be global (that is, provided by application developers rather than a template author), Python Liquid knows that forloop is in scope for the duration of the for block. If people were referenced before being assigned, we'd see an entry in the people list for each location where it is out of scope.

from liquid import Template

template = Template("""\
{{ people }}
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}
""")

analysis = template.analyze()

for name, location in analysis.global_variables.items():
for template_name, line_number in location:
print(f"'{name}' is out of scope in '{template_name}' on line {line_number}")
output
'people' is out of scope in '<string>' on line 1

Local Variables

The local_variables property of TemplateAnalysis is, again, a dictionary mapping template variable names to their locations. Each entry is the location of an assign, capture, increment, or decrement tag (or any custom tag that introduces names into the template local namespace) that initializes or updates the variable.

from liquid import Template

template = Template("""\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% assign people = "Bob, Frank" | split: ", " %}
""")

analysis = template.analyze()

for name, location in analysis.local_variables.items():
for template_name, line_number in location:
print(f"'{name}' assigned in '{template_name}' on line {line_number}")
output
'people' assigned in '<string>' on line 1
'people' assigned in '<string>' on line 2

Filters

New in version 1.7.0

The filters property of TemplateAnalysis is a dictionary mapping Liquid filter names to their locations. Undefined filters will be included in TemplateAnalysis.filters, regardless of whether strict_filters is set or not.

from liquid import Template

template = Template(
"""\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% for person in people %}
- {{ person | upcase | prepend: 'Hello, ' }}
{% endfor %}
"""
)

analysis = template.analyze()

for filter_name, location in analysis.filters.items():
for template_name, line_number in location:
print(f"'{filter_name}' found in '{template_name}' on line {line_number}")
output
'split' found in '<string>' on line 1
'upcase' found in '<string>' on line 3
'prepend' found in '<string>' on line 3

Tags

New in version 1.7.0

The tags property of TemplateAnalysis is a dictionary mapping Liquid tag names to their locations. Note that {% raw %} tags will never be included in TemplateAnalysis.tags. This is because the lexer converts them to template text before we get a chance to analyze them.

from liquid import Template

template = Template(
"""\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% for person in people %}
- {{ person | upcase | prepend: 'Hello, ' }}
{% endfor %}
"""
)

analysis = template.analyze()

for tag_name, location in analysis.tags.items():
for template_name, line_number in location:
print(f"'{tag_name}' found in '{template_name}' on line {line_number}")
'assign' found in '<string>' on line 1
'for' found in '<string>' on line 2

Analyzing Partial Templates

When the follow_partials argument to BoundTemplate.analyze() is True (the default), Python Liquid will attempt to load and analyze templates from include and render tags. In the case of include, this is only possible when the template name is a string literal.

from pprint import pprint
from liquid import Environment, DictLoader

templates = {
"layout": """\
{% include 'nav', title: page_name %}
{% render 'foot' with website as site_name %}
""",
"nav": "{{ title }} nav bar",
"foot": "a footer for {{ site_name }}",
}

env = Environment(loader=DictLoader(templates))
layout = env.get_template("layout")

analysis = layout.analyze(follow_partials=True)
pprint(analysis.variables)
output
{'page_name': [('layout', 1)],
'site_name': [('foot', 1)],
'title': [('nav', 1)],
'website': [('layout', 2)]}

When the raise_for_failures argument is True (the default), we should expect a TemplateTraversalError to be raised if a partial template can not be loaded. If raise_for_failures is False, a dictionary of unloadable include/render tags is available as TemplateAnalysis.unloadable_partials.

from liquid import Environment, DictLoader

templates = {
"layout": """\
{% include 'nav', title: page_name %}
{% render 'foot' with website as site_name %}
""",
}

env = Environment(loader=DictLoader(templates))
layout = env.get_template("layout")

analysis = layout.analyze(follow_partials=True, raise_for_failures=False)
print(analysis.unloadable_partials)
{'foot': [('layout', 2)], 'nav': [('layout', 1)]}

Analyzing Custom Tags

All built-in tags (the tag's Node and Expression classes) implement a children() method. When analyzing a custom tag that does not implement children(), and with the raise_for_failures argument set to True (the default), Python Liquid will raise a TemplateTraversalError. When raise_for_failures is False, a dictionary of unvisitable AST nodes and expressions is available as TemplateAnalysis.failed_visits.

from liquid import Environment, DictLoader
from liquid.ast import Node
from liquid.tag import Tag

class ExampleNode(Node):
def __init__(self, token: Token) -> None:
self.tok = token

def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
buffer.write("example node")

async def render_to_output_async(
self, context: Context, buffer: TextIO
) -> Optional[bool]:
buffer.write("example node")


class ExampleTag(Tag):
block = False
name = "example"

def parse(self, stream: TokenStream) -> Node:
return ExampleNode(stream.current)


templates = {
"layout": "{% example %}"
}

env = Environment(loader=DictLoader(templates))
env.add_tag(ExampleTag)
layout = env.get_template("layout")

analysis = layout.analyze(follow_partials=True, raise_for_failures=False)
print(analysis.failed_visits)
{'ExampleNode': [('layout', 1)]}

liquid.ast.Node.children() should return a list of liquid.ast.ChildNode objects. Each ChildNode includes a child Expression and/or Node, plus any names the tag adds to the template local scope or subsequent block scope. Please see liquid/builtin/tags for examples.

liquid.expression.Expression.children() is expected to return a list of child Expressions. For example, liquid.expression.RangeLiteral returns a list containing expressions for its start and stop properties. Please see liquid/expression.py for examples.