1222 lines
48 KiB
Python
1222 lines
48 KiB
Python
# mako/codegen.py
|
|
# Copyright (C) 2006-2013 the Mako authors and contributors <see AUTHORS file>
|
|
#
|
|
# This module is part of Mako and is released under
|
|
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
|
|
|
"""provides functionality for rendering a parsetree constructing into module
|
|
source code."""
|
|
|
|
import time
|
|
import re
|
|
from mako.pygen import PythonPrinter
|
|
from mako import util, ast, parsetree, filters, exceptions
|
|
from mako import compat
|
|
|
|
|
|
MAGIC_NUMBER = 9
|
|
|
|
# names which are hardwired into the
|
|
# template and are not accessed via the
|
|
# context itself
|
|
RESERVED_NAMES = set(['context', 'loop', 'UNDEFINED'])
|
|
|
|
def compile(node,
|
|
uri,
|
|
filename=None,
|
|
default_filters=None,
|
|
buffer_filters=None,
|
|
imports=None,
|
|
future_imports=None,
|
|
source_encoding=None,
|
|
generate_magic_comment=True,
|
|
disable_unicode=False,
|
|
strict_undefined=False,
|
|
enable_loop=True,
|
|
reserved_names=frozenset()):
|
|
|
|
"""Generate module source code given a parsetree node,
|
|
uri, and optional source filename"""
|
|
|
|
# if on Py2K, push the "source_encoding" string to be
|
|
# a bytestring itself, as we will be embedding it into
|
|
# the generated source and we don't want to coerce the
|
|
# result into a unicode object, in "disable_unicode" mode
|
|
if not compat.py3k and isinstance(source_encoding, compat.text_type):
|
|
source_encoding = source_encoding.encode(source_encoding)
|
|
|
|
|
|
buf = util.FastEncodingBuffer()
|
|
|
|
printer = PythonPrinter(buf)
|
|
_GenerateRenderMethod(printer,
|
|
_CompileContext(uri,
|
|
filename,
|
|
default_filters,
|
|
buffer_filters,
|
|
imports,
|
|
future_imports,
|
|
source_encoding,
|
|
generate_magic_comment,
|
|
disable_unicode,
|
|
strict_undefined,
|
|
enable_loop,
|
|
reserved_names),
|
|
node)
|
|
return buf.getvalue()
|
|
|
|
class _CompileContext(object):
|
|
def __init__(self,
|
|
uri,
|
|
filename,
|
|
default_filters,
|
|
buffer_filters,
|
|
imports,
|
|
future_imports,
|
|
source_encoding,
|
|
generate_magic_comment,
|
|
disable_unicode,
|
|
strict_undefined,
|
|
enable_loop,
|
|
reserved_names):
|
|
self.uri = uri
|
|
self.filename = filename
|
|
self.default_filters = default_filters
|
|
self.buffer_filters = buffer_filters
|
|
self.imports = imports
|
|
self.future_imports = future_imports
|
|
self.source_encoding = source_encoding
|
|
self.generate_magic_comment = generate_magic_comment
|
|
self.disable_unicode = disable_unicode
|
|
self.strict_undefined = strict_undefined
|
|
self.enable_loop = enable_loop
|
|
self.reserved_names = reserved_names
|
|
|
|
class _GenerateRenderMethod(object):
|
|
"""A template visitor object which generates the
|
|
full module source for a template.
|
|
|
|
"""
|
|
def __init__(self, printer, compiler, node):
|
|
self.printer = printer
|
|
self.last_source_line = -1
|
|
self.compiler = compiler
|
|
self.node = node
|
|
self.identifier_stack = [None]
|
|
self.in_def = isinstance(node, (parsetree.DefTag, parsetree.BlockTag))
|
|
|
|
if self.in_def:
|
|
name = "render_%s" % node.funcname
|
|
args = node.get_argument_expressions()
|
|
filtered = len(node.filter_args.args) > 0
|
|
buffered = eval(node.attributes.get('buffered', 'False'))
|
|
cached = eval(node.attributes.get('cached', 'False'))
|
|
defs = None
|
|
pagetag = None
|
|
if node.is_block and not node.is_anonymous:
|
|
args += ['**pageargs']
|
|
else:
|
|
defs = self.write_toplevel()
|
|
pagetag = self.compiler.pagetag
|
|
name = "render_body"
|
|
if pagetag is not None:
|
|
args = pagetag.body_decl.get_argument_expressions()
|
|
if not pagetag.body_decl.kwargs:
|
|
args += ['**pageargs']
|
|
cached = eval(pagetag.attributes.get('cached', 'False'))
|
|
self.compiler.enable_loop = self.compiler.enable_loop or eval(
|
|
pagetag.attributes.get(
|
|
'enable_loop', 'False')
|
|
)
|
|
else:
|
|
args = ['**pageargs']
|
|
cached = False
|
|
buffered = filtered = False
|
|
if args is None:
|
|
args = ['context']
|
|
else:
|
|
args = [a for a in ['context'] + args]
|
|
|
|
self.write_render_callable(
|
|
pagetag or node,
|
|
name, args,
|
|
buffered, filtered, cached)
|
|
|
|
if defs is not None:
|
|
for node in defs:
|
|
_GenerateRenderMethod(printer, compiler, node)
|
|
|
|
@property
|
|
def identifiers(self):
|
|
return self.identifier_stack[-1]
|
|
|
|
def write_toplevel(self):
|
|
"""Traverse a template structure for module-level directives and
|
|
generate the start of module-level code.
|
|
|
|
"""
|
|
inherit = []
|
|
namespaces = {}
|
|
module_code = []
|
|
|
|
self.compiler.pagetag = None
|
|
|
|
class FindTopLevel(object):
|
|
def visitInheritTag(s, node):
|
|
inherit.append(node)
|
|
def visitNamespaceTag(s, node):
|
|
namespaces[node.name] = node
|
|
def visitPageTag(s, node):
|
|
self.compiler.pagetag = node
|
|
def visitCode(s, node):
|
|
if node.ismodule:
|
|
module_code.append(node)
|
|
|
|
f = FindTopLevel()
|
|
for n in self.node.nodes:
|
|
n.accept_visitor(f)
|
|
|
|
self.compiler.namespaces = namespaces
|
|
|
|
module_ident = set()
|
|
for n in module_code:
|
|
module_ident = module_ident.union(n.declared_identifiers())
|
|
|
|
module_identifiers = _Identifiers(self.compiler)
|
|
module_identifiers.declared = module_ident
|
|
|
|
# module-level names, python code
|
|
if self.compiler.generate_magic_comment and \
|
|
self.compiler.source_encoding:
|
|
self.printer.writeline("# -*- coding:%s -*-" %
|
|
self.compiler.source_encoding)
|
|
|
|
if self.compiler.future_imports:
|
|
self.printer.writeline("from __future__ import %s" %
|
|
(", ".join(self.compiler.future_imports),))
|
|
self.printer.writeline("from mako import runtime, filters, cache")
|
|
self.printer.writeline("UNDEFINED = runtime.UNDEFINED")
|
|
self.printer.writeline("__M_dict_builtin = dict")
|
|
self.printer.writeline("__M_locals_builtin = locals")
|
|
self.printer.writeline("_magic_number = %r" % MAGIC_NUMBER)
|
|
self.printer.writeline("_modified_time = %r" % time.time())
|
|
self.printer.writeline("_enable_loop = %r" % self.compiler.enable_loop)
|
|
self.printer.writeline(
|
|
"_template_filename = %r" % self.compiler.filename)
|
|
self.printer.writeline("_template_uri = %r" % self.compiler.uri)
|
|
self.printer.writeline(
|
|
"_source_encoding = %r" % self.compiler.source_encoding)
|
|
if self.compiler.imports:
|
|
buf = ''
|
|
for imp in self.compiler.imports:
|
|
buf += imp + "\n"
|
|
self.printer.writeline(imp)
|
|
impcode = ast.PythonCode(
|
|
buf,
|
|
source='', lineno=0,
|
|
pos=0,
|
|
filename='template defined imports')
|
|
else:
|
|
impcode = None
|
|
|
|
main_identifiers = module_identifiers.branch(self.node)
|
|
module_identifiers.topleveldefs = \
|
|
module_identifiers.topleveldefs.\
|
|
union(main_identifiers.topleveldefs)
|
|
module_identifiers.declared.add("UNDEFINED")
|
|
if impcode:
|
|
module_identifiers.declared.update(impcode.declared_identifiers)
|
|
|
|
self.compiler.identifiers = module_identifiers
|
|
self.printer.writeline("_exports = %r" %
|
|
[n.name for n in
|
|
main_identifiers.topleveldefs.values()]
|
|
)
|
|
self.printer.write("\n\n")
|
|
|
|
if len(module_code):
|
|
self.write_module_code(module_code)
|
|
|
|
if len(inherit):
|
|
self.write_namespaces(namespaces)
|
|
self.write_inherit(inherit[-1])
|
|
elif len(namespaces):
|
|
self.write_namespaces(namespaces)
|
|
|
|
return list(main_identifiers.topleveldefs.values())
|
|
|
|
def write_render_callable(self, node, name, args, buffered, filtered,
|
|
cached):
|
|
"""write a top-level render callable.
|
|
|
|
this could be the main render() method or that of a top-level def."""
|
|
|
|
if self.in_def:
|
|
decorator = node.decorator
|
|
if decorator:
|
|
self.printer.writeline(
|
|
"@runtime._decorate_toplevel(%s)" % decorator)
|
|
|
|
self.printer.writelines(
|
|
"def %s(%s):" % (name, ','.join(args)),
|
|
# push new frame, assign current frame to __M_caller
|
|
"__M_caller = context.caller_stack._push_frame()",
|
|
"try:"
|
|
)
|
|
if buffered or filtered or cached:
|
|
self.printer.writeline("context._push_buffer()")
|
|
|
|
self.identifier_stack.append(
|
|
self.compiler.identifiers.branch(self.node))
|
|
if (not self.in_def or self.node.is_block) and '**pageargs' in args:
|
|
self.identifier_stack[-1].argument_declared.add('pageargs')
|
|
|
|
if not self.in_def and (
|
|
len(self.identifiers.locally_assigned) > 0 or
|
|
len(self.identifiers.argument_declared) > 0
|
|
):
|
|
self.printer.writeline("__M_locals = __M_dict_builtin(%s)" %
|
|
','.join([
|
|
"%s=%s" % (x, x) for x in
|
|
self.identifiers.argument_declared
|
|
]))
|
|
|
|
self.write_variable_declares(self.identifiers, toplevel=True)
|
|
|
|
for n in self.node.nodes:
|
|
n.accept_visitor(self)
|
|
|
|
self.write_def_finish(self.node, buffered, filtered, cached)
|
|
self.printer.writeline(None)
|
|
self.printer.write("\n\n")
|
|
if cached:
|
|
self.write_cache_decorator(
|
|
node, name,
|
|
args, buffered,
|
|
self.identifiers, toplevel=True)
|
|
|
|
def write_module_code(self, module_code):
|
|
"""write module-level template code, i.e. that which
|
|
is enclosed in <%! %> tags in the template."""
|
|
for n in module_code:
|
|
self.write_source_comment(n)
|
|
self.printer.write_indented_block(n.text)
|
|
|
|
def write_inherit(self, node):
|
|
"""write the module-level inheritance-determination callable."""
|
|
|
|
self.printer.writelines(
|
|
"def _mako_inherit(template, context):",
|
|
"_mako_generate_namespaces(context)",
|
|
"return runtime._inherit_from(context, %s, _template_uri)" %
|
|
(node.parsed_attributes['file']),
|
|
None
|
|
)
|
|
|
|
def write_namespaces(self, namespaces):
|
|
"""write the module-level namespace-generating callable."""
|
|
self.printer.writelines(
|
|
"def _mako_get_namespace(context, name):",
|
|
"try:",
|
|
"return context.namespaces[(__name__, name)]",
|
|
"except KeyError:",
|
|
"_mako_generate_namespaces(context)",
|
|
"return context.namespaces[(__name__, name)]",
|
|
None, None
|
|
)
|
|
self.printer.writeline("def _mako_generate_namespaces(context):")
|
|
|
|
|
|
for node in namespaces.values():
|
|
if 'import' in node.attributes:
|
|
self.compiler.has_ns_imports = True
|
|
self.write_source_comment(node)
|
|
if len(node.nodes):
|
|
self.printer.writeline("def make_namespace():")
|
|
export = []
|
|
identifiers = self.compiler.identifiers.branch(node)
|
|
self.in_def = True
|
|
class NSDefVisitor(object):
|
|
def visitDefTag(s, node):
|
|
s.visitDefOrBase(node)
|
|
|
|
def visitBlockTag(s, node):
|
|
s.visitDefOrBase(node)
|
|
|
|
def visitDefOrBase(s, node):
|
|
if node.is_anonymous:
|
|
raise exceptions.CompileException(
|
|
"Can't put anonymous blocks inside "
|
|
"<%namespace>",
|
|
**node.exception_kwargs
|
|
)
|
|
self.write_inline_def(node, identifiers, nested=False)
|
|
export.append(node.funcname)
|
|
vis = NSDefVisitor()
|
|
for n in node.nodes:
|
|
n.accept_visitor(vis)
|
|
self.printer.writeline("return [%s]" % (','.join(export)))
|
|
self.printer.writeline(None)
|
|
self.in_def = False
|
|
callable_name = "make_namespace()"
|
|
else:
|
|
callable_name = "None"
|
|
|
|
if 'file' in node.parsed_attributes:
|
|
self.printer.writeline(
|
|
"ns = runtime.TemplateNamespace(%r,"
|
|
" context._clean_inheritance_tokens(),"
|
|
" templateuri=%s, callables=%s, "
|
|
" calling_uri=_template_uri)" %
|
|
(
|
|
node.name,
|
|
node.parsed_attributes.get('file', 'None'),
|
|
callable_name,
|
|
)
|
|
)
|
|
elif 'module' in node.parsed_attributes:
|
|
self.printer.writeline(
|
|
"ns = runtime.ModuleNamespace(%r,"
|
|
" context._clean_inheritance_tokens(),"
|
|
" callables=%s, calling_uri=_template_uri,"
|
|
" module=%s)" %
|
|
(
|
|
node.name,
|
|
callable_name,
|
|
node.parsed_attributes.get('module', 'None')
|
|
)
|
|
)
|
|
else:
|
|
self.printer.writeline(
|
|
"ns = runtime.Namespace(%r,"
|
|
" context._clean_inheritance_tokens(),"
|
|
" callables=%s, calling_uri=_template_uri)" %
|
|
(
|
|
node.name,
|
|
callable_name,
|
|
)
|
|
)
|
|
if eval(node.attributes.get('inheritable', "False")):
|
|
self.printer.writeline("context['self'].%s = ns" % (node.name))
|
|
|
|
self.printer.writeline(
|
|
"context.namespaces[(__name__, %s)] = ns" % repr(node.name))
|
|
self.printer.write("\n")
|
|
if not len(namespaces):
|
|
self.printer.writeline("pass")
|
|
self.printer.writeline(None)
|
|
|
|
def write_variable_declares(self, identifiers, toplevel=False, limit=None):
|
|
"""write variable declarations at the top of a function.
|
|
|
|
the variable declarations are in the form of callable
|
|
definitions for defs and/or name lookup within the
|
|
function's context argument. the names declared are based
|
|
on the names that are referenced in the function body,
|
|
which don't otherwise have any explicit assignment
|
|
operation. names that are assigned within the body are
|
|
assumed to be locally-scoped variables and are not
|
|
separately declared.
|
|
|
|
for def callable definitions, if the def is a top-level
|
|
callable then a 'stub' callable is generated which wraps
|
|
the current Context into a closure. if the def is not
|
|
top-level, it is fully rendered as a local closure.
|
|
|
|
"""
|
|
|
|
# collection of all defs available to us in this scope
|
|
comp_idents = dict([(c.funcname, c) for c in identifiers.defs])
|
|
to_write = set()
|
|
|
|
# write "context.get()" for all variables we are going to
|
|
# need that arent in the namespace yet
|
|
to_write = to_write.union(identifiers.undeclared)
|
|
|
|
# write closure functions for closures that we define
|
|
# right here
|
|
to_write = to_write.union(
|
|
[c.funcname for c in identifiers.closuredefs.values()])
|
|
|
|
# remove identifiers that are declared in the argument
|
|
# signature of the callable
|
|
to_write = to_write.difference(identifiers.argument_declared)
|
|
|
|
# remove identifiers that we are going to assign to.
|
|
# in this way we mimic Python's behavior,
|
|
# i.e. assignment to a variable within a block
|
|
# means that variable is now a "locally declared" var,
|
|
# which cannot be referenced beforehand.
|
|
to_write = to_write.difference(identifiers.locally_declared)
|
|
|
|
if self.compiler.enable_loop:
|
|
has_loop = "loop" in to_write
|
|
to_write.discard("loop")
|
|
else:
|
|
has_loop = False
|
|
|
|
# if a limiting set was sent, constraint to those items in that list
|
|
# (this is used for the caching decorator)
|
|
if limit is not None:
|
|
to_write = to_write.intersection(limit)
|
|
|
|
if toplevel and getattr(self.compiler, 'has_ns_imports', False):
|
|
self.printer.writeline("_import_ns = {}")
|
|
self.compiler.has_imports = True
|
|
for ident, ns in self.compiler.namespaces.items():
|
|
if 'import' in ns.attributes:
|
|
self.printer.writeline(
|
|
"_mako_get_namespace(context, %r)."\
|
|
"_populate(_import_ns, %r)" %
|
|
(
|
|
ident,
|
|
re.split(r'\s*,\s*', ns.attributes['import'])
|
|
))
|
|
|
|
if has_loop:
|
|
self.printer.writeline(
|
|
'loop = __M_loop = runtime.LoopStack()'
|
|
)
|
|
|
|
for ident in to_write:
|
|
if ident in comp_idents:
|
|
comp = comp_idents[ident]
|
|
if comp.is_block:
|
|
if not comp.is_anonymous:
|
|
self.write_def_decl(comp, identifiers)
|
|
else:
|
|
self.write_inline_def(comp, identifiers, nested=True)
|
|
else:
|
|
if comp.is_root():
|
|
self.write_def_decl(comp, identifiers)
|
|
else:
|
|
self.write_inline_def(comp, identifiers, nested=True)
|
|
|
|
elif ident in self.compiler.namespaces:
|
|
self.printer.writeline(
|
|
"%s = _mako_get_namespace(context, %r)" %
|
|
(ident, ident)
|
|
)
|
|
else:
|
|
if getattr(self.compiler, 'has_ns_imports', False):
|
|
if self.compiler.strict_undefined:
|
|
self.printer.writelines(
|
|
"%s = _import_ns.get(%r, UNDEFINED)" %
|
|
(ident, ident),
|
|
"if %s is UNDEFINED:" % ident,
|
|
"try:",
|
|
"%s = context[%r]" % (ident, ident),
|
|
"except KeyError:",
|
|
"raise NameError(\"'%s' is not defined\")" %
|
|
ident,
|
|
None, None
|
|
)
|
|
else:
|
|
self.printer.writeline(
|
|
"%s = _import_ns.get(%r, context.get(%r, UNDEFINED))" %
|
|
(ident, ident, ident))
|
|
else:
|
|
if self.compiler.strict_undefined:
|
|
self.printer.writelines(
|
|
"try:",
|
|
"%s = context[%r]" % (ident, ident),
|
|
"except KeyError:",
|
|
"raise NameError(\"'%s' is not defined\")" %
|
|
ident,
|
|
None
|
|
)
|
|
else:
|
|
self.printer.writeline(
|
|
"%s = context.get(%r, UNDEFINED)" % (ident, ident)
|
|
)
|
|
|
|
self.printer.writeline("__M_writer = context.writer()")
|
|
|
|
def write_source_comment(self, node):
|
|
"""write a source comment containing the line number of the
|
|
corresponding template line."""
|
|
if self.last_source_line != node.lineno:
|
|
self.printer.writeline("# SOURCE LINE %d" % node.lineno)
|
|
self.last_source_line = node.lineno
|
|
|
|
def write_def_decl(self, node, identifiers):
|
|
"""write a locally-available callable referencing a top-level def"""
|
|
funcname = node.funcname
|
|
namedecls = node.get_argument_expressions()
|
|
nameargs = node.get_argument_expressions(include_defaults=False)
|
|
|
|
if not self.in_def and (
|
|
len(self.identifiers.locally_assigned) > 0 or
|
|
len(self.identifiers.argument_declared) > 0):
|
|
nameargs.insert(0, 'context._locals(__M_locals)')
|
|
else:
|
|
nameargs.insert(0, 'context')
|
|
self.printer.writeline("def %s(%s):" % (funcname, ",".join(namedecls)))
|
|
self.printer.writeline(
|
|
"return render_%s(%s)" % (funcname, ",".join(nameargs)))
|
|
self.printer.writeline(None)
|
|
|
|
def write_inline_def(self, node, identifiers, nested):
|
|
"""write a locally-available def callable inside an enclosing def."""
|
|
|
|
namedecls = node.get_argument_expressions()
|
|
|
|
decorator = node.decorator
|
|
if decorator:
|
|
self.printer.writeline(
|
|
"@runtime._decorate_inline(context, %s)" % decorator)
|
|
self.printer.writeline(
|
|
"def %s(%s):" % (node.funcname, ",".join(namedecls)))
|
|
filtered = len(node.filter_args.args) > 0
|
|
buffered = eval(node.attributes.get('buffered', 'False'))
|
|
cached = eval(node.attributes.get('cached', 'False'))
|
|
self.printer.writelines(
|
|
# push new frame, assign current frame to __M_caller
|
|
"__M_caller = context.caller_stack._push_frame()",
|
|
"try:"
|
|
)
|
|
if buffered or filtered or cached:
|
|
self.printer.writelines(
|
|
"context._push_buffer()",
|
|
)
|
|
|
|
identifiers = identifiers.branch(node, nested=nested)
|
|
|
|
self.write_variable_declares(identifiers)
|
|
|
|
self.identifier_stack.append(identifiers)
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
self.identifier_stack.pop()
|
|
|
|
self.write_def_finish(node, buffered, filtered, cached)
|
|
self.printer.writeline(None)
|
|
if cached:
|
|
self.write_cache_decorator(node, node.funcname,
|
|
namedecls, False, identifiers,
|
|
inline=True, toplevel=False)
|
|
|
|
def write_def_finish(self, node, buffered, filtered, cached,
|
|
callstack=True):
|
|
"""write the end section of a rendering function, either outermost or
|
|
inline.
|
|
|
|
this takes into account if the rendering function was filtered,
|
|
buffered, etc. and closes the corresponding try: block if any, and
|
|
writes code to retrieve captured content, apply filters, send proper
|
|
return value."""
|
|
|
|
if not buffered and not cached and not filtered:
|
|
self.printer.writeline("return ''")
|
|
if callstack:
|
|
self.printer.writelines(
|
|
"finally:",
|
|
"context.caller_stack._pop_frame()",
|
|
None
|
|
)
|
|
|
|
if buffered or filtered or cached:
|
|
if buffered or cached:
|
|
# in a caching scenario, don't try to get a writer
|
|
# from the context after popping; assume the caching
|
|
# implemenation might be using a context with no
|
|
# extra buffers
|
|
self.printer.writelines(
|
|
"finally:",
|
|
"__M_buf = context._pop_buffer()"
|
|
)
|
|
else:
|
|
self.printer.writelines(
|
|
"finally:",
|
|
"__M_buf, __M_writer = context._pop_buffer_and_writer()"
|
|
)
|
|
|
|
if callstack:
|
|
self.printer.writeline("context.caller_stack._pop_frame()")
|
|
|
|
s = "__M_buf.getvalue()"
|
|
if filtered:
|
|
s = self.create_filter_callable(node.filter_args.args, s,
|
|
False)
|
|
self.printer.writeline(None)
|
|
if buffered and not cached:
|
|
s = self.create_filter_callable(self.compiler.buffer_filters,
|
|
s, False)
|
|
if buffered or cached:
|
|
self.printer.writeline("return %s" % s)
|
|
else:
|
|
self.printer.writelines(
|
|
"__M_writer(%s)" % s,
|
|
"return ''"
|
|
)
|
|
|
|
def write_cache_decorator(self, node_or_pagetag, name,
|
|
args, buffered, identifiers,
|
|
inline=False, toplevel=False):
|
|
"""write a post-function decorator to replace a rendering
|
|
callable with a cached version of itself."""
|
|
|
|
self.printer.writeline("__M_%s = %s" % (name, name))
|
|
cachekey = node_or_pagetag.parsed_attributes.get('cache_key',
|
|
repr(name))
|
|
|
|
cache_args = {}
|
|
if self.compiler.pagetag is not None:
|
|
cache_args.update(
|
|
(
|
|
pa[6:],
|
|
self.compiler.pagetag.parsed_attributes[pa]
|
|
)
|
|
for pa in self.compiler.pagetag.parsed_attributes
|
|
if pa.startswith('cache_') and pa != 'cache_key'
|
|
)
|
|
cache_args.update(
|
|
(
|
|
pa[6:],
|
|
node_or_pagetag.parsed_attributes[pa]
|
|
) for pa in node_or_pagetag.parsed_attributes
|
|
if pa.startswith('cache_') and pa != 'cache_key'
|
|
)
|
|
if 'timeout' in cache_args:
|
|
cache_args['timeout'] = int(eval(cache_args['timeout']))
|
|
|
|
self.printer.writeline("def %s(%s):" % (name, ','.join(args)))
|
|
|
|
# form "arg1, arg2, arg3=arg3, arg4=arg4", etc.
|
|
pass_args = [
|
|
'=' in a and "%s=%s" % ((a.split('=')[0],)*2) or a
|
|
for a in args
|
|
]
|
|
|
|
self.write_variable_declares(
|
|
identifiers,
|
|
toplevel=toplevel,
|
|
limit=node_or_pagetag.undeclared_identifiers()
|
|
)
|
|
if buffered:
|
|
s = "context.get('local')."\
|
|
"cache._ctx_get_or_create("\
|
|
"%s, lambda:__M_%s(%s), context, %s__M_defname=%r)" % \
|
|
(cachekey, name, ','.join(pass_args),
|
|
''.join(["%s=%s, " % (k, v)
|
|
for k, v in cache_args.items()]),
|
|
name
|
|
)
|
|
# apply buffer_filters
|
|
s = self.create_filter_callable(self.compiler.buffer_filters, s,
|
|
False)
|
|
self.printer.writelines("return " + s, None)
|
|
else:
|
|
self.printer.writelines(
|
|
"__M_writer(context.get('local')."
|
|
"cache._ctx_get_or_create("\
|
|
"%s, lambda:__M_%s(%s), context, %s__M_defname=%r))" %
|
|
(cachekey, name, ','.join(pass_args),
|
|
''.join(["%s=%s, " % (k, v)
|
|
for k, v in cache_args.items()]),
|
|
name,
|
|
),
|
|
"return ''",
|
|
None
|
|
)
|
|
|
|
def create_filter_callable(self, args, target, is_expression):
|
|
"""write a filter-applying expression based on the filters
|
|
present in the given filter names, adjusting for the global
|
|
'default' filter aliases as needed."""
|
|
|
|
def locate_encode(name):
|
|
if re.match(r'decode\..+', name):
|
|
return "filters." + name
|
|
elif self.compiler.disable_unicode:
|
|
return filters.NON_UNICODE_ESCAPES.get(name, name)
|
|
else:
|
|
return filters.DEFAULT_ESCAPES.get(name, name)
|
|
|
|
if 'n' not in args:
|
|
if is_expression:
|
|
if self.compiler.pagetag:
|
|
args = self.compiler.pagetag.filter_args.args + args
|
|
if self.compiler.default_filters:
|
|
args = self.compiler.default_filters + args
|
|
for e in args:
|
|
# if filter given as a function, get just the identifier portion
|
|
if e == 'n':
|
|
continue
|
|
m = re.match(r'(.+?)(\(.*\))', e)
|
|
if m:
|
|
(ident, fargs) = m.group(1,2)
|
|
f = locate_encode(ident)
|
|
e = f + fargs
|
|
else:
|
|
x = e
|
|
e = locate_encode(e)
|
|
assert e is not None
|
|
target = "%s(%s)" % (e, target)
|
|
return target
|
|
|
|
def visitExpression(self, node):
|
|
self.write_source_comment(node)
|
|
if len(node.escapes) or \
|
|
(
|
|
self.compiler.pagetag is not None and
|
|
len(self.compiler.pagetag.filter_args.args)
|
|
) or \
|
|
len(self.compiler.default_filters):
|
|
|
|
s = self.create_filter_callable(node.escapes_code.args,
|
|
"%s" % node.text, True)
|
|
self.printer.writeline("__M_writer(%s)" % s)
|
|
else:
|
|
self.printer.writeline("__M_writer(%s)" % node.text)
|
|
|
|
def visitControlLine(self, node):
|
|
if node.isend:
|
|
self.printer.writeline(None)
|
|
if node.has_loop_context:
|
|
self.printer.writeline('finally:')
|
|
self.printer.writeline("loop = __M_loop._exit()")
|
|
self.printer.writeline(None)
|
|
else:
|
|
self.write_source_comment(node)
|
|
if self.compiler.enable_loop and node.keyword == 'for':
|
|
text = mangle_mako_loop(node, self.printer)
|
|
else:
|
|
text = node.text
|
|
self.printer.writeline(text)
|
|
children = node.get_children()
|
|
# this covers the three situations where we want to insert a pass:
|
|
# 1) a ternary control line with no children,
|
|
# 2) a primary control line with nothing but its own ternary
|
|
# and end control lines, and
|
|
# 3) any control line with no content other than comments
|
|
if not children or (
|
|
compat.all(isinstance(c, (parsetree.Comment,
|
|
parsetree.ControlLine))
|
|
for c in children) and
|
|
compat.all((node.is_ternary(c.keyword) or c.isend)
|
|
for c in children
|
|
if isinstance(c, parsetree.ControlLine))):
|
|
self.printer.writeline("pass")
|
|
|
|
def visitText(self, node):
|
|
self.write_source_comment(node)
|
|
self.printer.writeline("__M_writer(%s)" % repr(node.content))
|
|
|
|
def visitTextTag(self, node):
|
|
filtered = len(node.filter_args.args) > 0
|
|
if filtered:
|
|
self.printer.writelines(
|
|
"__M_writer = context._push_writer()",
|
|
"try:",
|
|
)
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
if filtered:
|
|
self.printer.writelines(
|
|
"finally:",
|
|
"__M_buf, __M_writer = context._pop_buffer_and_writer()",
|
|
"__M_writer(%s)" %
|
|
self.create_filter_callable(
|
|
node.filter_args.args,
|
|
"__M_buf.getvalue()",
|
|
False),
|
|
None
|
|
)
|
|
|
|
def visitCode(self, node):
|
|
if not node.ismodule:
|
|
self.write_source_comment(node)
|
|
self.printer.write_indented_block(node.text)
|
|
|
|
if not self.in_def and len(self.identifiers.locally_assigned) > 0:
|
|
# if we are the "template" def, fudge locally
|
|
# declared/modified variables into the "__M_locals" dictionary,
|
|
# which is used for def calls within the same template,
|
|
# to simulate "enclosing scope"
|
|
self.printer.writeline(
|
|
'__M_locals_builtin_stored = __M_locals_builtin()')
|
|
self.printer.writeline(
|
|
'__M_locals.update(__M_dict_builtin([(__M_key,'
|
|
' __M_locals_builtin_stored[__M_key]) for __M_key in'
|
|
' [%s] if __M_key in __M_locals_builtin_stored]))' %
|
|
','.join([repr(x) for x in node.declared_identifiers()]))
|
|
|
|
def visitIncludeTag(self, node):
|
|
self.write_source_comment(node)
|
|
args = node.attributes.get('args')
|
|
if args:
|
|
self.printer.writeline(
|
|
"runtime._include_file(context, %s, _template_uri, %s)" %
|
|
(node.parsed_attributes['file'], args))
|
|
else:
|
|
self.printer.writeline(
|
|
"runtime._include_file(context, %s, _template_uri)" %
|
|
(node.parsed_attributes['file']))
|
|
|
|
def visitNamespaceTag(self, node):
|
|
pass
|
|
|
|
def visitDefTag(self, node):
|
|
pass
|
|
|
|
def visitBlockTag(self, node):
|
|
if node.is_anonymous:
|
|
self.printer.writeline("%s()" % node.funcname)
|
|
else:
|
|
nameargs = node.get_argument_expressions(include_defaults=False)
|
|
nameargs += ['**pageargs']
|
|
self.printer.writeline("if 'parent' not in context._data or "
|
|
"not hasattr(context._data['parent'], '%s'):"
|
|
% node.funcname)
|
|
self.printer.writeline(
|
|
"context['self'].%s(%s)" % (node.funcname, ",".join(nameargs)))
|
|
self.printer.writeline("\n")
|
|
|
|
def visitCallNamespaceTag(self, node):
|
|
# TODO: we can put namespace-specific checks here, such
|
|
# as ensure the given namespace will be imported,
|
|
# pre-import the namespace, etc.
|
|
self.visitCallTag(node)
|
|
|
|
def visitCallTag(self, node):
|
|
self.printer.writeline("def ccall(caller):")
|
|
export = ['body']
|
|
callable_identifiers = self.identifiers.branch(node, nested=True)
|
|
body_identifiers = callable_identifiers.branch(node, nested=False)
|
|
# we want the 'caller' passed to ccall to be used
|
|
# for the body() function, but for other non-body()
|
|
# <%def>s within <%call> we want the current caller
|
|
# off the call stack (if any)
|
|
body_identifiers.add_declared('caller')
|
|
|
|
self.identifier_stack.append(body_identifiers)
|
|
class DefVisitor(object):
|
|
def visitDefTag(s, node):
|
|
s.visitDefOrBase(node)
|
|
|
|
def visitBlockTag(s, node):
|
|
s.visitDefOrBase(node)
|
|
|
|
def visitDefOrBase(s, node):
|
|
self.write_inline_def(node, callable_identifiers, nested=False)
|
|
if not node.is_anonymous:
|
|
export.append(node.funcname)
|
|
# remove defs that are within the <%call> from the
|
|
# "closuredefs" defined in the body, so they dont render twice
|
|
if node.funcname in body_identifiers.closuredefs:
|
|
del body_identifiers.closuredefs[node.funcname]
|
|
|
|
vis = DefVisitor()
|
|
for n in node.nodes:
|
|
n.accept_visitor(vis)
|
|
self.identifier_stack.pop()
|
|
|
|
bodyargs = node.body_decl.get_argument_expressions()
|
|
self.printer.writeline("def body(%s):" % ','.join(bodyargs))
|
|
|
|
# TODO: figure out best way to specify
|
|
# buffering/nonbuffering (at call time would be better)
|
|
buffered = False
|
|
if buffered:
|
|
self.printer.writelines(
|
|
"context._push_buffer()",
|
|
"try:"
|
|
)
|
|
self.write_variable_declares(body_identifiers)
|
|
self.identifier_stack.append(body_identifiers)
|
|
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
self.identifier_stack.pop()
|
|
|
|
self.write_def_finish(node, buffered, False, False, callstack=False)
|
|
self.printer.writelines(
|
|
None,
|
|
"return [%s]" % (','.join(export)),
|
|
None
|
|
)
|
|
|
|
self.printer.writelines(
|
|
# push on caller for nested call
|
|
"context.caller_stack.nextcaller = "
|
|
"runtime.Namespace('caller', context, "
|
|
"callables=ccall(__M_caller))",
|
|
"try:")
|
|
self.write_source_comment(node)
|
|
self.printer.writelines(
|
|
"__M_writer(%s)" % self.create_filter_callable(
|
|
[], node.expression, True),
|
|
"finally:",
|
|
"context.caller_stack.nextcaller = None",
|
|
None
|
|
)
|
|
|
|
class _Identifiers(object):
|
|
"""tracks the status of identifier names as template code is rendered."""
|
|
|
|
def __init__(self, compiler, node=None, parent=None, nested=False):
|
|
if parent is not None:
|
|
# if we are the branch created in write_namespaces(),
|
|
# we don't share any context from the main body().
|
|
if isinstance(node, parsetree.NamespaceTag):
|
|
self.declared = set()
|
|
self.topleveldefs = util.SetLikeDict()
|
|
else:
|
|
# things that have already been declared
|
|
# in an enclosing namespace (i.e. names we can just use)
|
|
self.declared = set(parent.declared).\
|
|
union([c.name for c in parent.closuredefs.values()]).\
|
|
union(parent.locally_declared).\
|
|
union(parent.argument_declared)
|
|
|
|
# if these identifiers correspond to a "nested"
|
|
# scope, it means whatever the parent identifiers
|
|
# had as undeclared will have been declared by that parent,
|
|
# and therefore we have them in our scope.
|
|
if nested:
|
|
self.declared = self.declared.union(parent.undeclared)
|
|
|
|
# top level defs that are available
|
|
self.topleveldefs = util.SetLikeDict(**parent.topleveldefs)
|
|
else:
|
|
self.declared = set()
|
|
self.topleveldefs = util.SetLikeDict()
|
|
|
|
self.compiler = compiler
|
|
|
|
# things within this level that are referenced before they
|
|
# are declared (e.g. assigned to)
|
|
self.undeclared = set()
|
|
|
|
# things that are declared locally. some of these things
|
|
# could be in the "undeclared" list as well if they are
|
|
# referenced before declared
|
|
self.locally_declared = set()
|
|
|
|
# assignments made in explicit python blocks.
|
|
# these will be propagated to
|
|
# the context of local def calls.
|
|
self.locally_assigned = set()
|
|
|
|
# things that are declared in the argument
|
|
# signature of the def callable
|
|
self.argument_declared = set()
|
|
|
|
# closure defs that are defined in this level
|
|
self.closuredefs = util.SetLikeDict()
|
|
|
|
self.node = node
|
|
|
|
if node is not None:
|
|
node.accept_visitor(self)
|
|
|
|
illegal_names = self.compiler.reserved_names.intersection(
|
|
self.locally_declared)
|
|
if illegal_names:
|
|
raise exceptions.NameConflictError(
|
|
"Reserved words declared in template: %s" %
|
|
", ".join(illegal_names))
|
|
|
|
|
|
def branch(self, node, **kwargs):
|
|
"""create a new Identifiers for a new Node, with
|
|
this Identifiers as the parent."""
|
|
|
|
return _Identifiers(self.compiler, node, self, **kwargs)
|
|
|
|
@property
|
|
def defs(self):
|
|
return set(self.topleveldefs.union(self.closuredefs).values())
|
|
|
|
def __repr__(self):
|
|
return "Identifiers(declared=%r, locally_declared=%r, "\
|
|
"undeclared=%r, topleveldefs=%r, closuredefs=%r, "\
|
|
"argumentdeclared=%r)" %\
|
|
(
|
|
list(self.declared),
|
|
list(self.locally_declared),
|
|
list(self.undeclared),
|
|
[c.name for c in self.topleveldefs.values()],
|
|
[c.name for c in self.closuredefs.values()],
|
|
self.argument_declared)
|
|
|
|
def check_declared(self, node):
|
|
"""update the state of this Identifiers with the undeclared
|
|
and declared identifiers of the given node."""
|
|
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and\
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
for ident in node.declared_identifiers():
|
|
self.locally_declared.add(ident)
|
|
|
|
def add_declared(self, ident):
|
|
self.declared.add(ident)
|
|
if ident in self.undeclared:
|
|
self.undeclared.remove(ident)
|
|
|
|
def visitExpression(self, node):
|
|
self.check_declared(node)
|
|
|
|
def visitControlLine(self, node):
|
|
self.check_declared(node)
|
|
|
|
def visitCode(self, node):
|
|
if not node.ismodule:
|
|
self.check_declared(node)
|
|
self.locally_assigned = self.locally_assigned.union(
|
|
node.declared_identifiers())
|
|
|
|
def visitNamespaceTag(self, node):
|
|
# only traverse into the sub-elements of a
|
|
# <%namespace> tag if we are the branch created in
|
|
# write_namespaces()
|
|
if self.node is node:
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
|
|
def _check_name_exists(self, collection, node):
|
|
existing = collection.get(node.funcname)
|
|
collection[node.funcname] = node
|
|
if existing is not None and \
|
|
existing is not node and \
|
|
(node.is_block or existing.is_block):
|
|
raise exceptions.CompileException(
|
|
"%%def or %%block named '%s' already "
|
|
"exists in this template." %
|
|
node.funcname, **node.exception_kwargs)
|
|
|
|
def visitDefTag(self, node):
|
|
if node.is_root() and not node.is_anonymous:
|
|
self._check_name_exists(self.topleveldefs, node)
|
|
elif node is not self.node:
|
|
self._check_name_exists(self.closuredefs, node)
|
|
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and\
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
|
|
# visit defs only one level deep
|
|
if node is self.node:
|
|
for ident in node.declared_identifiers():
|
|
self.argument_declared.add(ident)
|
|
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
|
|
def visitBlockTag(self, node):
|
|
if node is not self.node and \
|
|
not node.is_anonymous:
|
|
|
|
if isinstance(self.node, parsetree.DefTag):
|
|
raise exceptions.CompileException(
|
|
"Named block '%s' not allowed inside of def '%s'"
|
|
% (node.name, self.node.name), **node.exception_kwargs)
|
|
elif isinstance(self.node,
|
|
(parsetree.CallTag, parsetree.CallNamespaceTag)):
|
|
raise exceptions.CompileException(
|
|
"Named block '%s' not allowed inside of <%%call> tag"
|
|
% (node.name, ), **node.exception_kwargs)
|
|
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and \
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
|
|
if not node.is_anonymous:
|
|
self._check_name_exists(self.topleveldefs, node)
|
|
self.undeclared.add(node.funcname)
|
|
elif node is not self.node:
|
|
self._check_name_exists(self.closuredefs, node)
|
|
for ident in node.declared_identifiers():
|
|
self.argument_declared.add(ident)
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
|
|
def visitTextTag(self, node):
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and \
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
|
|
def visitIncludeTag(self, node):
|
|
self.check_declared(node)
|
|
|
|
def visitPageTag(self, node):
|
|
for ident in node.declared_identifiers():
|
|
self.argument_declared.add(ident)
|
|
self.check_declared(node)
|
|
|
|
def visitCallNamespaceTag(self, node):
|
|
self.visitCallTag(node)
|
|
|
|
def visitCallTag(self, node):
|
|
if node is self.node:
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and\
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
for ident in node.declared_identifiers():
|
|
self.argument_declared.add(ident)
|
|
for n in node.nodes:
|
|
n.accept_visitor(self)
|
|
else:
|
|
for ident in node.undeclared_identifiers():
|
|
if ident != 'context' and\
|
|
ident not in self.declared.union(self.locally_declared):
|
|
self.undeclared.add(ident)
|
|
|
|
|
|
_FOR_LOOP = re.compile(
|
|
r'^for\s+((?:\(?)\s*[A-Za-z_][A-Za-z_0-9]*'
|
|
r'(?:\s*,\s*(?:[A-Za-z_][A-Za-z0-9_]*),??)*\s*(?:\)?))\s+in\s+(.*):'
|
|
)
|
|
|
|
def mangle_mako_loop(node, printer):
|
|
"""converts a for loop into a context manager wrapped around a for loop
|
|
when access to the `loop` variable has been detected in the for loop body
|
|
"""
|
|
loop_variable = LoopVariable()
|
|
node.accept_visitor(loop_variable)
|
|
if loop_variable.detected:
|
|
node.nodes[-1].has_loop_context = True
|
|
match = _FOR_LOOP.match(node.text)
|
|
if match:
|
|
printer.writelines(
|
|
'loop = __M_loop._enter(%s)' % match.group(2),
|
|
'try:'
|
|
#'with __M_loop(%s) as loop:' % match.group(2)
|
|
)
|
|
text = 'for %s in loop:' % match.group(1)
|
|
else:
|
|
raise SyntaxError("Couldn't apply loop context: %s" % node.text)
|
|
else:
|
|
text = node.text
|
|
return text
|
|
|
|
|
|
class LoopVariable(object):
|
|
"""A node visitor which looks for the name 'loop' within undeclared
|
|
identifiers."""
|
|
|
|
def __init__(self):
|
|
self.detected = False
|
|
|
|
def _loop_reference_detected(self, node):
|
|
if 'loop' in node.undeclared_identifiers():
|
|
self.detected = True
|
|
else:
|
|
for n in node.get_children():
|
|
n.accept_visitor(self)
|
|
|
|
def visitControlLine(self, node):
|
|
self._loop_reference_detected(node)
|
|
|
|
def visitCode(self, node):
|
|
self._loop_reference_detected(node)
|
|
|
|
def visitExpression(self, node):
|
|
self._loop_reference_detected(node)
|