You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
2 years ago
|
"""Functions that expose information about templates that might be
|
||
|
interesting for introspection.
|
||
|
"""
|
||
|
import typing as t
|
||
|
|
||
|
from . import nodes
|
||
|
from .compiler import CodeGenerator
|
||
|
from .compiler import Frame
|
||
|
|
||
|
if t.TYPE_CHECKING:
|
||
|
from .environment import Environment
|
||
|
|
||
|
|
||
|
class TrackingCodeGenerator(CodeGenerator):
|
||
|
"""We abuse the code generator for introspection."""
|
||
|
|
||
|
def __init__(self, environment: "Environment") -> None:
|
||
|
super().__init__(environment, "<introspection>", "<introspection>")
|
||
|
self.undeclared_identifiers: t.Set[str] = set()
|
||
|
|
||
|
def write(self, x: str) -> None:
|
||
|
"""Don't write."""
|
||
|
|
||
|
def enter_frame(self, frame: Frame) -> None:
|
||
|
"""Remember all undeclared identifiers."""
|
||
|
super().enter_frame(frame)
|
||
|
|
||
|
for _, (action, param) in frame.symbols.loads.items():
|
||
|
if action == "resolve" and param not in self.environment.globals:
|
||
|
self.undeclared_identifiers.add(param)
|
||
|
|
||
|
|
||
|
def find_undeclared_variables(ast: nodes.Template) -> t.Set[str]:
|
||
|
"""Returns a set of all variables in the AST that will be looked up from
|
||
|
the context at runtime. Because at compile time it's not known which
|
||
|
variables will be used depending on the path the execution takes at
|
||
|
runtime, all variables are returned.
|
||
|
|
||
|
>>> from jinja2 import Environment, meta
|
||
|
>>> env = Environment()
|
||
|
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}')
|
||
|
>>> meta.find_undeclared_variables(ast) == {'bar'}
|
||
|
True
|
||
|
|
||
|
.. admonition:: Implementation
|
||
|
|
||
|
Internally the code generator is used for finding undeclared variables.
|
||
|
This is good to know because the code generator might raise a
|
||
|
:exc:`TemplateAssertionError` during compilation and as a matter of
|
||
|
fact this function can currently raise that exception as well.
|
||
|
"""
|
||
|
codegen = TrackingCodeGenerator(ast.environment) # type: ignore
|
||
|
codegen.visit(ast)
|
||
|
return codegen.undeclared_identifiers
|
||
|
|
||
|
|
||
|
_ref_types = (nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include)
|
||
|
_RefType = t.Union[nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include]
|
||
|
|
||
|
|
||
|
def find_referenced_templates(ast: nodes.Template) -> t.Iterator[t.Optional[str]]:
|
||
|
"""Finds all the referenced templates from the AST. This will return an
|
||
|
iterator over all the hardcoded template extensions, inclusions and
|
||
|
imports. If dynamic inheritance or inclusion is used, `None` will be
|
||
|
yielded.
|
||
|
|
||
|
>>> from jinja2 import Environment, meta
|
||
|
>>> env = Environment()
|
||
|
>>> ast = env.parse('{% extends "layout.html" %}{% include helper %}')
|
||
|
>>> list(meta.find_referenced_templates(ast))
|
||
|
['layout.html', None]
|
||
|
|
||
|
This function is useful for dependency tracking. For example if you want
|
||
|
to rebuild parts of the website after a layout template has changed.
|
||
|
"""
|
||
|
template_name: t.Any
|
||
|
|
||
|
for node in ast.find_all(_ref_types):
|
||
|
template: nodes.Expr = node.template # type: ignore
|
||
|
|
||
|
if not isinstance(template, nodes.Const):
|
||
|
# a tuple with some non consts in there
|
||
|
if isinstance(template, (nodes.Tuple, nodes.List)):
|
||
|
for template_name in template.items:
|
||
|
# something const, only yield the strings and ignore
|
||
|
# non-string consts that really just make no sense
|
||
|
if isinstance(template_name, nodes.Const):
|
||
|
if isinstance(template_name.value, str):
|
||
|
yield template_name.value
|
||
|
# something dynamic in there
|
||
|
else:
|
||
|
yield None
|
||
|
# something dynamic we don't know about here
|
||
|
else:
|
||
|
yield None
|
||
|
continue
|
||
|
# constant is a basestring, direct template name
|
||
|
if isinstance(template.value, str):
|
||
|
yield template.value
|
||
|
# a tuple or list (latter *should* not happen) made of consts,
|
||
|
# yield the consts that are strings. We could warn here for
|
||
|
# non string values
|
||
|
elif isinstance(node, nodes.Include) and isinstance(
|
||
|
template.value, (tuple, list)
|
||
|
):
|
||
|
for template_name in template.value:
|
||
|
if isinstance(template_name, str):
|
||
|
yield template_name
|
||
|
# something else we don't care about, we could warn here
|
||
|
else:
|
||
|
yield None
|