Skip to content
Snippets Groups Projects
codegen.py 24.5 KiB
Newer Older
Mike Bayer's avatar
Mike Bayer committed
# codegen.py
# Copyright (C) 2006 Michael Bayer mike_mp@zzzcomputing.com
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

Mike Bayer's avatar
Mike Bayer committed
"""provides the Compiler object for generating module source code."""

import re
Mike Bayer's avatar
Mike Bayer committed
from mako.pygen import PythonPrinter
from mako import util, ast, parsetree, filters
Mike Bayer's avatar
Mike Bayer committed

Mike Bayer's avatar
Mike Bayer committed
class Compiler(object):
Mike Bayer's avatar
Mike Bayer committed
    def __init__(self, node, filename=None):
Mike Bayer's avatar
Mike Bayer committed
        self.node = node
Mike Bayer's avatar
Mike Bayer committed
        self.filename = filename
Mike Bayer's avatar
Mike Bayer committed
    def render(self):
        buf = util.FastEncodingBuffer()
Mike Bayer's avatar
Mike Bayer committed
        printer = PythonPrinter(buf)
        _GenerateRenderMethod(printer, self, self.node)
Mike Bayer's avatar
Mike Bayer committed
        return buf.getvalue()
Mike Bayer's avatar
Mike Bayer committed
class _GenerateRenderMethod(object):
Mike Bayer's avatar
Mike Bayer committed
    """a template visitor object which generates the full module source for a template."""
    def __init__(self, printer, compiler, node):
Mike Bayer's avatar
Mike Bayer committed
        self.printer = printer
        self.last_source_line = -1

        self.in_def = isinstance(node, parsetree.DefTag)

        if self.in_def:
            name = "render_" + node.name
            args = node.function_decl.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
            (pagetag, defs) = self.write_toplevel()
            buffered = filtered = False
            cached = pagetag is not None and eval(pagetag.attributes.get('cached', 'False'))
Mike Bayer's avatar
Mike Bayer committed
        if args is None:
Mike Bayer's avatar
Mike Bayer committed
        else:
            args = [a for a in ['context'] + args + ['**kwargs']]
        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)
            
    def write_toplevel(self):
Mike Bayer's avatar
Mike Bayer committed
        """traverse a template structure for module-level directives and generate the
        start of module-level code."""
        inherit = []
        namespaces = {}
        module_code = []
        pagetag = [None]
        encoding =[None]

        class FindTopLevel(object):
            def visitInheritTag(s, node):
                inherit.append(node)
            def visitNamespaceTag(self, node):
                namespaces[node.name] = node
            def visitPageTag(self, node):
                pagetag[0] = node
            def visitCode(self, 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 = util.Set()
            module_ident = module_ident.union(n.declared_identifiers())
        module_identifiers = _Identifiers()
        module_identifiers.declared = module_ident
        
        self.printer.writeline("from mako import runtime, filters, cache")
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("UNDEFINED = runtime.UNDEFINED")
        self.printer.writeline("_magic_number = %s" % repr(MAGIC_NUMBER))
        self.printer.writeline("_modified_time = %s" % repr(time.time()))
        self.printer.writeline("_template_filename=%s" % repr(self.compiler.filename))
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("_template_cache=cache.Cache(__name__, _modified_time)")
        main_identifiers = module_identifiers.branch(self.node)
        module_identifiers.topleveldefs = module_identifiers.topleveldefs.union(main_identifiers.topleveldefs)
        [module_identifiers.declared.add(x) for x in ["UNDEFINED"]]
        self.compiler.identifiers = module_identifiers
        self.printer.writeline("_exports = %s" % repr([n.name for n in main_identifiers.topleveldefs]))
        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 (pagetag[0], main_identifiers.topleveldefs)
    def write_render_callable(self, node, name, args, buffered, filtered, cached):
Mike Bayer's avatar
Mike Bayer committed
        """write a top-level render callable.
        
        this could be the main render() method or that of a top-level def."""
        self.printer.writeline("def %s(%s):" % (name, ','.join(args)))
        if buffered or filtered or cached:
            self.printer.writeline("context.push_buffer()")
            self.printer.writeline("try:")

        self.identifiers = self.compiler.identifiers.branch(self.node)
        if not self.in_def and len(self.identifiers.locally_assigned) > 0:
            self.printer.writeline("__locals = {}")
        self.write_variable_declares(self.identifiers, toplevel=True)
        self.write_def_finish(self.node, buffered, filtered, cached)
        self.printer.writeline(None)
        self.printer.write("\n\n")
            self.write_cache_decorator(node, name, buffered, self.identifiers)
        
    def write_module_code(self, module_code):
Mike Bayer's avatar
Mike Bayer committed
        """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):
Mike Bayer's avatar
Mike Bayer committed
        """write the module-level inheritance-determination callable."""
        self.printer.writeline("def _mako_inherit(template, context):")
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("_mako_generate_namespaces(context)")
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("return runtime.inherit_from(context, %s, _template_filename)" % (node.parsed_attributes['file']))
    def write_namespaces(self, namespaces):
Mike Bayer's avatar
Mike Bayer committed
        """write the module-level namespace-generating callable."""
Mike Bayer's avatar
Mike Bayer committed
            "def _mako_get_namespace(context, name):",
            "try:",
            "return context.namespaces[(render, name)]",
            "except KeyError:",
Mike Bayer's avatar
Mike Bayer committed
            "_mako_generate_namespaces(context)",
            "return context.namespaces[(render, name)]",
            None,None
            )
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("def _mako_generate_namespaces(context):")
            if node.attributes.has_key('import'):
                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)
                class NSDefVisitor(object):
                    def visitDefTag(s, node):
                        self.write_inline_def(node, identifiers, nested=False)
                        export.append(node.name)
                vis = NSDefVisitor()
                for n in node.nodes:
                    n.accept_visitor(vis)
                self.printer.writeline("return [%s]" % (','.join(export)))
                self.printer.writeline(None)
                callable_name = "make_namespace()"
            else:
                callable_name = "None"
            self.printer.writeline("ns = runtime.Namespace(%s, context.clean_inheritance_tokens(), templateuri=%s, callables=%s, calling_filename=_template_filename)" % (repr(node.name), node.parsed_attributes.get('file', 'None'), callable_name))
            if eval(node.attributes.get('inheritable', "False")):
                self.printer.writeline("context['self'].%s = ns" % (node.name))
            self.printer.writeline("context.namespaces[(render, %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.
        
Mike Bayer's avatar
Mike Bayer committed
        the variable declarations are in the form of callable definitions for defs and/or
Mike Bayer's avatar
Mike Bayer committed
        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.
Mike Bayer's avatar
Mike Bayer committed
        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
Mike Bayer's avatar
Mike Bayer committed
        is not top-level, it is fully rendered as a local closure."""
Mike Bayer's avatar
Mike Bayer committed
        # collection of all defs available to us in this scope
        comp_idents = dict([(c.name, c) for c in identifiers.defs])
        
        # 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
Mike Bayer's avatar
Mike Bayer committed
        to_write = to_write.union(util.Set([c.name for c in identifiers.closuredefs]))

        # remove identifiers that are declared in the argument signature of the callable
Mike Bayer's avatar
Mike Bayer committed
        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 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.iteritems():
                if ns.attributes.has_key('import'):
                    self.printer.writeline("_mako_get_namespace(context, %s).populate(_import_ns, %s)" % (repr(ident),  repr(re.split(r'\s*,\s*', ns.attributes['import']))))
                        
        for ident in to_write:
            if ident in comp_idents:
                comp = comp_idents[ident]
                if comp.is_root():
Mike Bayer's avatar
Mike Bayer committed
                    self.write_def_decl(comp, identifiers)
                    self.write_inline_def(comp, identifiers, nested=True)
            elif ident in self.compiler.namespaces:
Mike Bayer's avatar
Mike Bayer committed
                self.printer.writeline("%s = _mako_get_namespace(context, %s)" % (ident, repr(ident)))
                if getattr(self.compiler, 'has_ns_imports', False):
                    self.printer.writeline("%s = _import_ns.get(%s, kwargs.get(%s, context.get(%s, UNDEFINED)))" % (ident, repr(ident), repr(ident), repr(ident)))
                    self.printer.writeline("%s = kwargs.get(%s, context.get(%s, UNDEFINED))" % (ident, repr(ident), repr(ident)))
        
    def write_source_comment(self, node):
Mike Bayer's avatar
Mike Bayer committed
        """write a source comment containing the line number of the corresponding template line."""
Mike Bayer's avatar
Mike Bayer committed
        if self.last_source_line != node.lineno:
Mike Bayer's avatar
Mike Bayer committed
            self.printer.writeline("# SOURCE LINE %d" % node.lineno)
Mike Bayer's avatar
Mike Bayer committed
            self.last_source_line = node.lineno
Mike Bayer's avatar
Mike Bayer committed
    def write_def_decl(self, node, identifiers):
        """write a locally-available callable referencing a top-level def"""
        funcname = node.function_decl.funcname
        namedecls = node.function_decl.get_argument_expressions()
        nameargs = node.function_decl.get_argument_expressions(include_defaults=False)
        if not self.in_def and len(self.identifiers.locally_assigned) > 0:
            nameargs.insert(0, 'context.locals_(__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):
Mike Bayer's avatar
Mike Bayer committed
        """write a locally-available def callable inside an enclosing def."""
        namedecls = node.function_decl.get_argument_expressions()
        self.printer.writeline("def %s(%s):" % (node.name, ",".join(namedecls)))
        filtered = len(node.filter_args.args) > 0 
        buffered = eval(node.attributes.get('buffered', 'False'))
        cached = eval(node.attributes.get('cached', 'False'))
        if buffered or filtered or cached:
            printer.writelines(
                "context.push_buffer()",
                "try:"
                )
Mike Bayer's avatar
Mike Bayer committed

        identifiers = identifiers.branch(node, nested=nested)
        self.write_variable_declares(identifiers)
        for n in node.nodes:
            n.accept_visitor(self)
        self.write_def_finish(node, buffered, filtered, cached)
            self.write_cache_decorator(node, node.name, False, identifiers)
    def write_def_finish(self, node, buffered, filtered, cached):
Mike Bayer's avatar
Mike Bayer committed
        """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 buffered or filtered or cached:
            self.printer.writeline("finally:")
            self.printer.writeline("_buf = context.pop_buffer()")
            s = "_buf.getvalue()"
            if filtered:
                s = self.create_filter_callable(node.filter_args.args, s)
            if buffered or cached:
                self.printer.writeline("return %s" % s)
            else:
                self.printer.writeline("context.write(%s)" % s)
Mike Bayer's avatar
Mike Bayer committed
                self.printer.writeline("return ''")
    def write_cache_decorator(self, node_or_pagetag, name, buffered, identifiers):
Mike Bayer's avatar
Mike Bayer committed
        """write a post-function decorator to replace a rendering callable with a cached version of itself."""
        self.printer.writeline("__%s = %s" % (name, name))
        cachekey = node_or_pagetag.parsed_attributes.get('cache_key', repr(name))
        self.printer.writeline("def %s(context, *args, **kwargs):" % name)
        print "LIMIT", node_or_pagetag.undeclared_identifiers()
        self.write_variable_declares(identifiers, limit=node_or_pagetag.undeclared_identifiers())
        if buffered:
            self.printer.writelines(
                    "return _template_cache.get(%s, createfunc=lambda:__%s(context, *args, **kwargs))" % (cachekey, name),
                None
            )
        else:
            self.printer.writelines(
                    "context.write(_template_cache.get(%s, createfunc=lambda:__%s(context, *args, **kwargs)))" % (cachekey, name),
    def create_filter_callable(self, args, target):
Mike Bayer's avatar
Mike Bayer committed
        """write a filter-applying expression based on the filters present in the given 
        filter names, adjusting for the global 'default' filter aliases as needed."""
        d = dict([(k, "filters." + v.func_name) for k, v in filters.DEFAULT_ESCAPES.iteritems()])
        for e in args:
            e = d.get(e, e)
            target = "%s(%s)" % (e, target)
        return target
        
Mike Bayer's avatar
Mike Bayer committed
    def visitExpression(self, node):
        self.write_source_comment(node)
        if len(node.escapes):
            s = self.create_filter_callable(node.escapes_code.args, node.text)
            self.printer.writeline("context.write(unicode(%s))" % s)
        else:
            self.printer.writeline("context.write(unicode(%s))" % node.text)
            
Mike Bayer's avatar
Mike Bayer committed
    def visitControlLine(self, node):
        if node.isend:
            self.printer.writeline(None)
        else:
            self.write_source_comment(node)
Mike Bayer's avatar
Mike Bayer committed
            self.printer.writeline(node.text)
    def visitText(self, node):
        self.write_source_comment(node)
Mike Bayer's avatar
Mike Bayer committed
        self.printer.writeline("context.write(%s)" % repr(node.content))
    def visitTextTag(self, node):
        filtered = len(node.filter_args.args) > 0
        if filtered:
            self.printer.writelines(
                "context.push_buffer()",
                "try:",
            )
        for n in node.nodes:
            n.accept_visitor(self)
        if filtered:
            self.printer.writelines(
                "finally:",
                "_buf = context.pop_buffer()",
                "context.write(%s)" % self.create_filter_callable(node.filter_args.args, "_buf.getvalue()"),
                None
                )
        
Mike Bayer's avatar
Mike Bayer committed
    def visitCode(self, node):
        if not node.ismodule:
            self.write_source_comment(node)
Mike Bayer's avatar
Mike Bayer committed
            self.printer.write_indented_block(node.text)
            if not self.in_def and len(self.identifiers.locally_assigned) > 0:
Mike Bayer's avatar
Mike Bayer committed
                # if we are the "template" def, fudge locally declared/modified variables into the "__locals" dictionary,
                # which is used for def calls within the same template, to simulate "enclosing scope"
                self.printer.writeline('__locals.update(dict([(k, v) for k, v in locals().iteritems() if k in [%s]]))' % ','.join([repr(x) for x in node.declared_identifiers()]))
                
Mike Bayer's avatar
Mike Bayer committed
    def visitIncludeTag(self, node):
        self.write_source_comment(node)
        self.printer.writeline("runtime.include_file(context, %s, _template_filename)" % (node.parsed_attributes['file']))
Mike Bayer's avatar
Mike Bayer committed

Mike Bayer's avatar
Mike Bayer committed
    def visitNamespaceTag(self, node):
Mike Bayer's avatar
Mike Bayer committed
    def visitDefTag(self, node):
Mike Bayer's avatar
Mike Bayer committed

Mike Bayer's avatar
Mike Bayer committed
    def visitCallTag(self, node):
        self.printer.writeline("def ccall(context):")
        export = ['body']
        identifiers = self.identifiers.branch(node, includedefs=True, nested=True)
        body_identifiers = identifiers.branch(node, includedefs=True, nested=False)
Mike Bayer's avatar
Mike Bayer committed
        class DefVisitor(object):
            def visitDefTag(s, node):
                self.write_inline_def(node, identifiers, nested=False)
                export.append(node.name)
Mike Bayer's avatar
Mike Bayer committed
        vis = DefVisitor()
        for n in node.nodes:
            n.accept_visitor(vis)
        self.printer.writeline("def body(**kwargs):")
        # 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)
        for n in node.nodes:
            n.accept_visitor(self)
        self.write_def_finish(node, buffered, False, False)
        self.printer.writelines(
            None,
            "return [%s]" % (','.join(export)),
            None
        )

        self.printer.writeline(
        # preserve local instance of current caller in local scope
        "__cl = context.locals_({'caller':context.caller_stack[-1]})",
        )

        self.printer.writelines(
            # push on global "caller" to be picked up by the next ccall
            "context.caller_stack.append(runtime.Namespace('caller', __cl, callables=ccall(__cl)))",
            # TODO: clean this up - insure proper caller is set
            "context._data['caller'] = runtime._StackFacade(context.caller_stack)",
            #"context.write('GOING TO CALL %s WITH CONTEXT ID '+ repr(id(context)) + ' CALLER ' + repr(context.get('caller')))" % node.attributes['expr'],
Mike Bayer's avatar
Mike Bayer committed
            "try:")
        self.write_source_comment(node)
        self.printer.writelines(
                "context.write(unicode(%s))" % node.attributes['expr'],
            "finally:",
                # pop it off
                "context.caller_stack.pop()",
    """tracks the status of identifier names as template code is rendered."""
    def __init__(self, node=None, parent=None, includedefs=True, includenode=True, nested=False):
            # things that have already been declared in an enclosing namespace (i.e. names we can just use)
Mike Bayer's avatar
Mike Bayer committed
            self.declared = util.Set(parent.declared).union([c.name for c in parent.closuredefs]).union(parent.locally_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)
            
Mike Bayer's avatar
Mike Bayer committed
            # top level defs that are available
            self.topleveldefs = util.Set(parent.topleveldefs)
Mike Bayer's avatar
Mike Bayer committed
            self.topleveldefs = util.Set()
        # things within this level that are referenced before they are declared (e.g. assigned to)
        self.undeclared = util.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 = util.Set()
Mike Bayer's avatar
Mike Bayer committed
    
        # assignments made in explicit python blocks.  these will be propigated to 
Mike Bayer's avatar
Mike Bayer committed
        # the context of local def calls.
        self.locally_assigned = util.Set()
        
Mike Bayer's avatar
Mike Bayer committed
        # things that are declared in the argument signature of the def callable
Mike Bayer's avatar
Mike Bayer committed
        self.argument_declared = util.Set()
Mike Bayer's avatar
Mike Bayer committed
        # closure defs that are defined in this level
        self.closuredefs = util.Set()
Mike Bayer's avatar
Mike Bayer committed
        self.includedefs = includedefs
            if includenode:
                node.accept_visitor(self)
            else:
                for n in node.nodes:
                    n.accept_visitor(self)
    def branch(self, node, **kwargs):
        """create a new Identifiers for a new Node, with this Identifiers as the parent."""
        return _Identifiers(node, self, **kwargs)
Mike Bayer's avatar
Mike Bayer committed
    defs = property(lambda s:s.topleveldefs.union(s.closuredefs))
Mike Bayer's avatar
Mike Bayer committed
        return "Identifiers(%s, %s, %s, %s, %s)" % (repr(list(self.declared)), repr(list(self.locally_declared)), repr(list(self.undeclared)), repr([c.name for c in self.topleveldefs]), repr([c.name for c in self.closuredefs]))
        """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):
        for ident in node.declared_identifiers():
            self.locally_declared.add(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())
Mike Bayer's avatar
Mike Bayer committed
    def visitDefTag(self, node):
        if not self.includedefs:
Mike Bayer's avatar
Mike Bayer committed
            self.topleveldefs.add(node)
        elif node is not self.node:
Mike Bayer's avatar
Mike Bayer committed
            self.closuredefs.add(node)
        for ident in node.undeclared_identifiers():
            if ident != 'context' and ident not in self.declared.union(self.locally_declared):
                self.undeclared.add(ident)
Mike Bayer's avatar
Mike Bayer committed
        for ident in node.declared_identifiers():
            self.argument_declared.add(ident)
Mike Bayer's avatar
Mike Bayer committed
        # visit defs only one level deep
        if node is self.node:
            for n in node.nodes:
                n.accept_visitor(self)
    def visitIncludeTag(self, node):
    def visitPageTag(self, node):
        self.check_declared(node)            
    def visitCallTag(self, node):
        self.check_declared(node)
        if node is self.node:
            for n in node.nodes:
                n.accept_visitor(self)