diff options
-rw-r--r-- | CHANGES | 3 | ||||
-rw-r--r-- | mako/codegen.py | 258 | ||||
-rw-r--r-- | test/test_template.py | 12 |
3 files changed, 207 insertions, 66 deletions
@@ -29,6 +29,9 @@ - A percent sign can be emitted as the first non-whitespace character on a line by escaping it as in "%%". [ticket:112] + +- Template accepts empty control structure, i.e. + % if: %endif, etc. [ticket:94] 0.2.6 diff --git a/mako/codegen.py b/mako/codegen.py index ded9396..71a608f 100644 --- a/mako/codegen.py +++ b/mako/codegen.py @@ -211,8 +211,6 @@ class _GenerateRenderMethod(object): return main_identifiers.topleveldefs.values() -##### continue [ticket:98] below #### - def write_render_callable(self, node, name, args, buffered, filtered, cached): """write a top-level render callable. @@ -235,8 +233,15 @@ class _GenerateRenderMethod(object): if not self.in_def 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])) + 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) @@ -247,21 +252,26 @@ class _GenerateRenderMethod(object): self.printer.writeline(None) self.printer.write("\n\n") if cached: - self.write_cache_decorator(node, name, args, buffered, self.identifiers, toplevel=True) + 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.""" + """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']), + "return runtime._inherit_from(context, %s, _template_uri)" % + (node.parsed_attributes['file']), None ) @@ -297,9 +307,18 @@ class _GenerateRenderMethod(object): callable_name = "make_namespace()" else: callable_name = "None" - self.printer.writeline("ns = runtime.Namespace(%s, context._clean_inheritance_tokens(), templateuri=%s, callables=%s, calling_uri=_template_uri, module=%s)" % (repr(node.name), node.parsed_attributes.get('file', 'None'), callable_name, node.parsed_attributes.get('module', 'None'))) + self.printer.writeline( + "ns = runtime.Namespace(%r, context._clean_inheritance_tokens()," + " templateuri=%s, callables=%s, calling_uri=_template_uri, module=%s)" % + ( + node.name, + node.parsed_attributes.get('file', 'None'), + callable_name, + node.parsed_attributes.get('module', 'None')) + ) 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): @@ -309,31 +328,42 @@ class _GenerateRenderMethod(object): 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. + 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.""" + 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.name, 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 + # 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(set([c.name for c in identifiers.closuredefs.values()])) + # write closure functions for closures that we define + # right here + to_write = to_write.union([c.name for c in identifiers.closuredefs.values()]) - # remove identifiers that are declared in the argument signature of the callable + # 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, + # 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) @@ -347,7 +377,12 @@ class _GenerateRenderMethod(object): 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'])))) + self.printer.writeline( + "_mako_get_namespace(context, %r)._populate(_import_ns, %r)" % + ( + ident, + re.split(r'\s*,\s*', ns.attributes['import']) + )) for ident in to_write: if ident in comp_idents: @@ -357,12 +392,17 @@ class _GenerateRenderMethod(object): else: self.write_inline_def(comp, identifiers, nested=True) elif ident in self.compiler.namespaces: - self.printer.writeline("%s = _mako_get_namespace(context, %s)" % (ident, repr(ident))) + self.printer.writeline( + "%s = _mako_get_namespace(context, %r)" % + (ident, ident) + ) else: if getattr(self.compiler, 'has_ns_imports', False): - self.printer.writeline("%s = _import_ns.get(%s, context.get(%s, UNDEFINED))" % (ident, repr(ident), repr(ident))) + self.printer.writeline( + "%s = _import_ns.get(%r, context.get(%r, UNDEFINED))" % + (ident, ident, ident)) else: - self.printer.writeline("%s = context.get(%s, UNDEFINED)" % (ident, repr(ident))) + self.printer.writeline("%s = context.get(%r, UNDEFINED)" % (ident, ident)) self.printer.writeline("__M_writer = context.writer()") @@ -377,7 +417,10 @@ class _GenerateRenderMethod(object): 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 or len(self.identifiers.argument_declared) > 0): + + 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') @@ -387,6 +430,7 @@ class _GenerateRenderMethod(object): def write_inline_def(self, node, identifiers, nested): """write a locally-available def callable inside an enclosing def.""" + namedecls = node.function_decl.get_argument_expressions() decorator = node.decorator @@ -417,14 +461,17 @@ class _GenerateRenderMethod(object): self.write_def_finish(node, buffered, filtered, cached) self.printer.writeline(None) if cached: - self.write_cache_decorator(node, node.name, namedecls, False, identifiers, inline=True, toplevel=False) + self.write_cache_decorator(node, node.name, + 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.""" + 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: @@ -467,12 +514,18 @@ class _GenerateRenderMethod(object): "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.""" + 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)) cacheargs = {} - for arg in (('cache_type', 'type'), ('cache_dir', 'data_dir'), ('cache_timeout', 'expiretime'), ('cache_url', 'url')): + for arg in ( + ('cache_type', 'type'), ('cache_dir', 'data_dir'), + ('cache_timeout', 'expiretime'), ('cache_url', 'url')): val = node_or_pagetag.parsed_attributes.get(arg[0], None) if val is not None: if arg[1] == 'expiretime': @@ -491,24 +544,41 @@ class _GenerateRenderMethod(object): 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()) + 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').get_cached(%s, defname=%r, %screatefunc=lambda:__M_%s(%s))" % (cachekey, name, ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), name, ','.join(pass_args)) + s = "context.get('local')."\ + "get_cached(%s, defname=%r, %screatefunc=lambda:__M_%s(%s))" % \ + (cachekey, name, + ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), + name, ','.join(pass_args)) # 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').get_cached(%s, defname=%r, %screatefunc=lambda:__M_%s(%s)))" % (cachekey, name, ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), name, ','.join(pass_args)), + "__M_writer(context.get('local')." + "get_cached(%s, defname=%r, %screatefunc=lambda:__M_%s(%s)))" % + (cachekey, name, + ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), + name, ','.join(pass_args)), "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.""" + """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 @@ -539,7 +609,13 @@ class _GenerateRenderMethod(object): 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): + 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: @@ -547,13 +623,17 @@ class _GenerateRenderMethod(object): def visitControlLine(self, node): if node.isend: + if not node.get_children(): + self.printer.writeline("pass") self.printer.writeline(None) else: self.write_source_comment(node) self.printer.writeline(node.text) + 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: @@ -567,7 +647,11 @@ class _GenerateRenderMethod(object): 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), + "__M_writer(%s)" % + self.create_filter_callable( + node.filter_args.args, + "__M_buf.getvalue()", + False), None ) @@ -577,18 +661,28 @@ class _GenerateRenderMethod(object): 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" + # 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()])) + 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)) + 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'])) + self.printer.writeline( + "runtime._include_file(context, %s, _template_uri)" % + (node.parsed_attributes['file'])) def visitNamespaceTag(self, node): pass @@ -607,8 +701,10 @@ class _GenerateRenderMethod(object): 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) + # 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) @@ -628,7 +724,9 @@ class _GenerateRenderMethod(object): 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) + + # TODO: figure out best way to specify + # buffering/nonbuffering (at call time would be better) buffered = False if buffered: self.printer.writelines( @@ -653,7 +751,8 @@ class _GenerateRenderMethod(object): # get local reference to current caller, if any "caller = context.caller_stack._get_caller()", # push on caller for nested call - "context.caller_stack.nextcaller = runtime.Namespace('caller', context, callables=ccall(caller))", + "context.caller_stack.nextcaller = " + "runtime.Namespace('caller', context, callables=ccall(caller))", "try:") self.write_source_comment(node) self.printer.writelines( @@ -665,13 +764,19 @@ class _GenerateRenderMethod(object): class _Identifiers(object): """tracks the status of identifier names as template code is rendered.""" + def __init__(self, node=None, parent=None, nested=False): if parent is not None: - # 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) + # 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, + # 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) @@ -682,18 +787,22 @@ class _Identifiers(object): self.declared = set() self.topleveldefs = util.SetLikeDict() - # things within this level that are referenced before they are declared (e.g. assigned to) + # 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 + # 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 propigated to + # 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 + # things that are declared in the argument + # signature of the def callable self.argument_declared = set() # closure defs that are defined in this level @@ -705,16 +814,30 @@ class _Identifiers(object): node.accept_visitor(self) def branch(self, node, **kwargs): - """create a new Identifiers for a new Node, with this Identifiers as the parent.""" + """create a new Identifiers for a new Node, with + this Identifiers as the parent.""" + return _Identifiers(node, self, **kwargs) - defs = property(lambda self:set(self.topleveldefs.union(self.closuredefs).values())) + @property + def defs(self): + return set(self.topleveldefs.union(self.closuredefs).values()) def __repr__(self): - return "Identifiers(declared=%s, locally_declared=%s, undeclared=%s, topleveldefs=%s, closuredefs=%s, argumenetdeclared=%s)" % (repr(list(self.declared)), repr(list(self.locally_declared)), repr(list(self.undeclared)), repr([c.name for c in self.topleveldefs.values()]), repr([c.name for c in self.closuredefs.values()]), repr(self.argument_declared)) + 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.""" + """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) @@ -728,12 +851,15 @@ class _Identifiers(object): 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 visitDefTag(self, node): if node.is_root(): self.topleveldefs[node.name] = node @@ -748,8 +874,10 @@ class _Identifiers(object): self.argument_declared.add(ident) for n in node.nodes: n.accept_visitor(self) + def visitIncludeTag(self, node): self.check_declared(node) + def visitPageTag(self, node): for ident in node.declared_identifiers(): self.argument_declared.add(ident) diff --git a/test/test_template.py b/test/test_template.py index 970565a..204ee3b 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -509,7 +509,17 @@ class ControlTest(TemplateTest): "no x does not have test", "yes x has test" ] - + + def test_blank_control(self): + self._do_memory_test( + """ + % if True: + % endif + """, + "", + filters=lambda s:s.strip() + ) + def test_multiline_control(self): t = Template(""" % for x in \\ |