diff --git a/CHANGES b/CHANGES index 4798462c22a717ce4d865ad4ec4c86e9a3088f59..c81d5bcb1a41b9fcdb195e10dc20e9663fe3f365 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,11 @@ 0.2.5 +- Added a "decorator" kw argument to <%def>, + allows custom decoration functions to wrap + rendering callables. Mainly intended for + custom caching algorithms, not sure what + other uses there may be (but there may be). + Examples are in the "filtering" docs. + - Added last_modified accessor to Template, returns the time.time() when the module was created. [ticket:97] diff --git a/doc/build/content/defs.txt b/doc/build/content/defs.txt index a7e6bf5df6c1fe33ae9454ebbcc736175cf0c9b5..932597b40c5b67c6ce746b4ebcebb477ac5962ed 100644 --- a/doc/build/content/defs.txt +++ b/doc/build/content/defs.txt @@ -298,3 +298,4 @@ The above layout would produce: The number of things you can do with `<%call>` and/or the `<%namespacename:defname>` calling syntax is enormous. You can create form widget libraries, such as an enclosing `<FORM>` tag and nested HTML input elements, or portable wrapping schemes using `<div>` or other elements. You can create tags that interpret rows of data, such as from a database, providing the individual columns of each row to a `body()` callable which lays out the row any way it wants. Basically anything you'd do with a "custom tag" or tag library in some other system, Mako provides via `<%def>`s and plain Python callables which are invoked via `<%namespacename:defname>` or `<%call>`. + diff --git a/doc/build/content/filtering.txt b/doc/build/content/filtering.txt index 86a332f40090b8fd61c37f070f410585889ddaaf..83cca0dc5b2b0b700f3ed101cec033ff4d8ab5e8 100644 --- a/doc/build/content/filtering.txt +++ b/doc/build/content/filtering.txt @@ -161,4 +161,41 @@ The above call is equivalent to the unbuffered call: ${somedef(17, 'hi', use_paging=True)} +### Decorating + +This is a feature that's new as of version 0.2.5. Somewhat like a filter for a %def but more flexible, the `decorator` argument to `%def` allows the creation of a function that will work in a similar manner to a Python decorator. The function can control whether or not the function executes. The original intent of this function is to allow the creation of custom cache logic, but there may be other uses as well. + +`decorator` is intended to be used with a regular Python function, such as one defined in a library module. Here we'll illustrate the python function defined in the template for simplicities' sake: + + <%! + def bar(fn): + def decorate(context, *args, **kw): + context.write("BAR") + fn(*args, **kw) + context.write("BAR") + return '' + return decorate + %> + + <%def name="foo()" decorator="bar"> + this is foo + </%def> + + ${foo()} +The above template will return, with more whitespace than this, `"BAR this is foo BAR"`. The function is the render callable itself (or possibly a wrapper around it), and by default will write to the context. To capture its output, use the `capture` callable in the `mako.runtime` module (available in templates as just `runtime`): + + <%! + def bar(fn): + def decorate(context, *args, **kw): + return "BAR" + runtime.capture(context, fn, *args, **kw) + "BAR" + return decorate + %> + + <%def name="foo()" decorator="bar"> + this is foo + </%def> + + ${foo()} + +The decorator can be used with top-level defs as well as nested defs. Note that when calling a top-level def from the `Template` api, i.e. `template.get_def('somedef').render()`, the decorator has to write the output to the `context`, i.e. as in the first example. The return value gets discarded. \ No newline at end of file diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py index f02b8de1520a193e0c10b968018d615ee8825df9..077be5908bcdd5101c9e12583a09af8d8b0d4b2d 100644 --- a/lib/mako/codegen.py +++ b/lib/mako/codegen.py @@ -160,6 +160,12 @@ class _GenerateRenderMethod(object): """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)), "context.caller_stack._push_frame()", @@ -325,6 +331,10 @@ 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 + if decorator: + self.printer.writeline("@runtime._decorate_inline(context, %s)" % decorator) self.printer.writeline("def %s(%s):" % (node.name, ",".join(namedecls))) filtered = len(node.filter_args.args) > 0 buffered = eval(node.attributes.get('buffered', 'False')) diff --git a/lib/mako/parsetree.py b/lib/mako/parsetree.py index 8b53ff582262fec847901bc1e5a7d55b3013347c..f4dede7975436cfbfa62b82f5d53a61e128148b6 100644 --- a/lib/mako/parsetree.py +++ b/lib/mako/parsetree.py @@ -341,7 +341,7 @@ class DefTag(Tag): keyword, attributes, ('buffered', 'cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'cache_url'), - ('name','filter'), + ('name','filter', 'decorator'), ('name',), **kwargs) name = attributes['name'] @@ -349,6 +349,7 @@ class DefTag(Tag): raise exceptions.CompileException("Missing parenthesis in %def", **self.exception_kwargs) self.function_decl = ast.FunctionDecl("def " + name + ":pass", **self.exception_kwargs) self.name = self.function_decl.funcname + self.decorator = attributes.get('decorator', '') self.filter_args = ast.ArgumentList(attributes.get('filter', ''), **self.exception_kwargs) def declared_identifiers(self): diff --git a/lib/mako/runtime.py b/lib/mako/runtime.py index bd569ae2d05529f53024c111a26f5f8f1530f05e..f4ef5032f73bc3e401c8242f55fc98dbf4458cfb 100644 --- a/lib/mako/runtime.py +++ b/lib/mako/runtime.py @@ -282,7 +282,24 @@ def capture(context, callable_, *args, **kwargs): finally: buf = context._pop_buffer() return buf.getvalue() - + +def _decorate_toplevel(fn): + def decorate_render(render_fn): + def go(context, *args, **kw): + def y(*args, **kw): + return render_fn(context, *args, **kw) + return fn(y)(context, *args, **kw) + return go + return decorate_render + +def _decorate_inline(context, fn): + def decorate_render(render_fn): + dec = fn(render_fn) + def go(*args, **kw): + return dec(context, *args, **kw) + return go + return decorate_render + def _include_file(context, uri, calling_uri, **kwargs): """locate the template from the given uri and include it in the current output.""" template = _lookup_template(context, uri, calling_uri) diff --git a/test/alltests.py b/test/alltests.py index a60c8c93caa516ef4f0b718a992b66bece14ed4a..ff758e8110edadda5ef4545f448e4a92f11e607b 100644 --- a/test/alltests.py +++ b/test/alltests.py @@ -9,6 +9,7 @@ def suite(): 'template', 'lookup', 'def', + 'decorators', 'namespace', 'filters', 'inheritance', diff --git a/test/decorators.py b/test/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..cf14fa63cc3262f7d5a81b147f2ca34a7658f85c --- /dev/null +++ b/test/decorators.py @@ -0,0 +1,75 @@ +from mako.template import Template +from mako import lookup +import unittest +from util import flatten_result, result_lines + +class DecoratorTest(unittest.TestCase): + def test_toplevel(self): + template = Template(""" + <%! + def bar(fn): + def decorate(context, *args, **kw): + return "BAR" + runtime.capture(context, fn, *args, **kw) + "BAR" + return decorate + %> + + <%def name="foo(y, x)" decorator="bar"> + this is foo ${y} ${x} + </%def> + + ${foo(1, x=5)} + """) + + assert flatten_result(template.render()) == "BAR this is foo 1 5 BAR" + + def test_toplevel_contextual(self): + template = Template(""" + <%! + def bar(fn): + def decorate(context): + context.write("BAR") + fn() + context.write("BAR") + return '' + return decorate + %> + + <%def name="foo()" decorator="bar"> + this is foo + </%def> + + ${foo()} + """) + + assert flatten_result(template.render()) == "BAR this is foo BAR" + + assert flatten_result(template.get_def('foo').render()) == "BAR this is foo BAR" + + + def test_nested(self): + template = Template(""" + <%! + def bat(fn): + def decorate(context): + return "BAT" + runtime.capture(context, fn) + "BAT" + return decorate + %> + + <%def name="foo()"> + + <%def name="bar()" decorator="bat"> + this is bar + </%def> + ${bar()} + </%def> + + ${foo()} + """) + + assert flatten_result(template.render()) == "BAT this is bar BAT" + + + +if __name__ == '__main__': + unittest.main() + \ No newline at end of file