def generate(node, environment, filename, stream=None):
"""Generate the python source for a node tree."""
- is_child = node.find(nodes.Extends) is not None
- generator = CodeGenerator(environment, is_child, filename, stream)
+ generator = CodeGenerator(environment, filename, stream)
generator.visit(node)
if stream is None:
return generator.stream.getvalue()
def __init__(self, parent=None):
self.identifiers = Identifiers()
+
# a toplevel frame is the root + soft frames such as if conditions.
self.toplevel = False
+
# the root frame is basically just the outermost frame, so no if
# conditions. This information is used to optimize inheritance
# situations.
self.rootlevel = False
- self.parent = parent
+
+ # inside some tags we are using a buffer rather than yield statements.
+ # this for example affects {% filter %} or {% macro %}. If a frame
+ # is buffered this variable points to the name of the list used as
+ # buffer.
self.buffer = None
+
+ # if a frame has name_overrides, all read access to a name in this
+ # dict is redirected to a string expression.
self.name_overrides = {}
+
+ # the name of the block we're in, otherwise None.
self.block = parent and parent.block or None
+
+ # the parent of this frame
+ self.parent = parent
+
if parent is not None:
self.identifiers.declared.update(
parent.identifiers.declared |
class CodeGenerator(NodeVisitor):
- def __init__(self, environment, is_child, filename, stream=None):
+ def __init__(self, environment, filename, stream=None):
if stream is None:
stream = StringIO()
self.environment = environment
- self.is_child = is_child
self.filename = filename
self.stream = stream
+
+ # a registry for all blocks. Because blocks are moved out
+ # into the global python scope they are registered here
self.blocks = {}
- self.indentation = 0
- self.new_lines = 0
- self.last_identifier = 0
+
+ # the number of extends statements so far
self.extends_so_far = 0
+
+ # some templates have a rootlevel extends. In this case we
+ # can safely assume that we're a child template and do some
+ # more optimizations.
self.has_known_extends = False
+
+ # the number of new lines before the next write()
+ self._new_lines = 0
+
+ # the line number of the last written statement
self._last_line = 0
+
+ # true if nothing was written so far.
self._first_write = True
+ # used by the `temporary_identifier` method to get new
+ # unique, temporary identifier
+ self._last_identifier = 0
+
+ # the current indentation
+ self._indentation = 0
+
def temporary_identifier(self):
- self.last_identifier += 1
- return 't%d' % self.last_identifier
+ """Get a new unique identifier."""
+ self._last_identifier += 1
+ return 't%d' % self._last_identifier
def indent(self):
- self.indentation += 1
+ """Indent by one."""
+ self._indentation += 1
def outdent(self, step=1):
- self.indentation -= step
+ """Outdent by step."""
+ self._indentation -= step
def blockvisit(self, nodes, frame, indent=True, force_generator=True):
+ """Visit a list of nodes as block in a frame. Per default the
+ code is indented, but this can be disabled by setting the indent
+ parameter to False. If the current frame is no buffer a dummy
+ ``if 0: yield None`` is written automatically unless the
+ force_generator parameter is set to False.
+ """
if indent:
self.indent()
if frame.buffer is None and force_generator:
self.outdent()
def write(self, x):
- if self.new_lines:
+ """Write a string into the output stream."""
+ if self._new_lines:
if not self._first_write:
- self.stream.write('\n' * self.new_lines)
+ self.stream.write('\n' * self._new_lines)
self._first_write = False
- self.stream.write(' ' * self.indentation)
- self.new_lines = 0
+ self.stream.write(' ' * self._indentation)
+ self._new_lines = 0
self.stream.write(x)
def writeline(self, x, node=None, extra=0):
+ """Combination of newline and write."""
self.newline(node, extra)
self.write(x)
def newline(self, node=None, extra=0):
- self.new_lines = max(self.new_lines, 1 + extra)
+ """Add one or more newlines before the next write."""
+ self._new_lines = max(self._new_lines, 1 + extra)
if node is not None and node.lineno != self._last_line:
self.write('# line: %s' % node.lineno)
- self.new_lines = 1
+ self._new_lines = 1
self._last_line = node.lineno
def signature(self, node, frame, have_comma=True, extra_kwargs=None):
+ """Writes a function call to the stream for the current node.
+ Per default it will write a leading comma but this can be
+ disabled by setting have_comma to False. If extra_kwargs is
+ given it must be a string that represents a single keyword
+ argument call that is inserted at the end of the regular
+ keyword argument calls.
+ """
have_comma = have_comma and [True] or []
def touch_comma():
if have_comma:
self.visit(node.dyn_kwargs, frame)
def pull_locals(self, frame, indent=True):
+ """Pull all the references identifiers into the local scope.
+ This affects regular names, filters and tests. If indent is
+ set to False, no automatic indentation will take place.
+ """
if indent:
self.indent()
for name in frame.identifiers.undeclared:
self.outdent()
def collect_shadowed(self, frame):
+ """This function returns all the shadowed variables in a dict
+ in the form name: alias and will write the required assignments
+ into the current scope. No indentation takes place.
+ """
# make sure we "backup" overridden, local identifiers
# TODO: we should probably optimize this and check if the
# identifier is in use afterwards.
return aliases
def function_scoping(self, node, frame):
+ """In Jinja a few statements require the help of anonymous
+ functions. Those are currently macros and call blocks and in
+ the future also recursive loops. As there is currently
+ technical limitation that doesn't allow reading and writing a
+ variable in a scope where the initial value is coming from an
+ outer scope, this function tries to fall back with a common
+ error message. Additionally the frame passed is modified so
+ that the argumetns are collected and callers are looked up.
+
+ This will return the modified frame.
+ """
func_frame = frame.inner()
func_frame.inspect(node.iter_child_nodes(), hard_scope=True)
comment_end_string='#}',
line_statement_prefix=None,
trim_blocks=False,
+ optimized=True,
loader=None):
"""Here the possible initialization parameters:
`trim_blocks` If this is set to ``True`` the first newline
after a block is removed (block, not
variable tag!). Defaults to ``False``.
+ `optimized` should the optimizer be enabled? Default is
+ ``True``.
`loader` the loader which should be used.
========================= ============================================
"""
self.comment_end_string = comment_end_string
self.line_statement_prefix = line_statement_prefix
self.trim_blocks = trim_blocks
+ self.optimized = optimized
# defaults
self.filters = DEFAULT_FILTERS.copy()
"""
return self.lexer.tokeniter(source, filename)
- def compile(self, source, filename=None, raw=False):
+ def compile(self, source, filename=None, raw=False, globals=None):
"""Compile a node or source."""
if isinstance(source, basestring):
source = self.parse(source, filename)
- node = optimize(source, self)
+ if self.optimized:
+ node = optimize(source, self, globals or {})
source = generate(node, self, filename)
if raw:
return source
function can be used to calculate the real filename."""
return template
- def get_template(self, name, parent=None):
+ def get_template(self, name, parent=None, globals=None):
"""Load a template."""
if self.loader is None:
raise TypeError('no loader for this environment specified')
if parent is not None:
name = self.join_path(name, parent)
- return self.loader.load(self, name)
+ globals = self.make_globals(globals)
+ return self.loader.load(self, name, globals)
- def from_string(self, source, filename='<string>'):
+ def from_string(self, source, filename='<string>', globals=None):
"""Load a template from a string."""
- return Template(self, self.compile(source, filename))
+ globals = self.make_globals(globals)
+ return Template(self, self.compile(source, filename), globals)
+
+ def make_globals(self, d):
+ """Return a dict for the globals."""
+ if d is None:
+ return self.globals
+ return dict(self.globals, **d)
class Template(object):
"""Represents a template."""
- def __init__(self, environment, code):
+ def __init__(self, environment, code, globals):
namespace = {'environment': environment}
exec code in namespace
self.environment = environment
self.name = namespace['filename']
self.root_render_func = namespace['root']
self.blocks = namespace['blocks']
+ self.globals = globals
def render(self, *args, **kwargs):
- return u''.join(self.stream(*args, **kwargs))
+ return u''.join(self.generate(*args, **kwargs))
def stream(self, *args, **kwargs):
- gen = self.root_render_func(dict(*args, **kwargs))
+ return TemplateStream(self.generate(*args, **kwargs))
+
+ def generate(self, *args, **kwargs):
+ # assemble the context
+ context = self.globals.copy()
+ context.update(*args, **kwargs)
+
+ # if the environment is using the optimizer locals may never
+ # override globals as optimizations might have happened
+ # depending on values of certain globals. This assertion goes
+ # away if the python interpreter is started with -O
+ if __debug__ and self.environment.optimized:
+ overrides = set(context) & set(self.globals)
+ if overrides:
+ plural = len(overrides) != 1 and 's' or ''
+ raise AssertionError('the per template variable%s %s '
+ 'override%s global variable%s. '
+ 'With an enabled optimizer this '
+ 'will lead to unexpected results.' %
+ (plural, ', '.join(overrides), plural or ' a', plural))
+ gen = self.root_render_func(context)
# skip the first item which is a reference to the stream
gen.next()
return gen