From da73752bd69abb1ff53f964cf7990317124c3ca4 Mon Sep 17 00:00:00 2001
From: Mike Bayer <mike_mp@zzzcomputing.com>
Date: Thu, 19 Mar 2009 23:40:37 +0000
Subject: [PATCH] - 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.

---
 CHANGES                         |  7 +++
 doc/build/content/defs.txt      |  1 +
 doc/build/content/filtering.txt | 37 ++++++++++++++++
 lib/mako/codegen.py             | 10 +++++
 lib/mako/parsetree.py           |  3 +-
 lib/mako/runtime.py             | 19 ++++++++-
 test/alltests.py                |  1 +
 test/decorators.py              | 75 +++++++++++++++++++++++++++++++++
 8 files changed, 151 insertions(+), 2 deletions(-)
 create mode 100644 test/decorators.py

diff --git a/CHANGES b/CHANGES
index 4798462..c81d5bc 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 a7e6bf5..932597b 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 86a332f..83cca0d 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 f02b8de..077be59 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 8b53ff5..f4dede7 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 bd569ae..f4ef503 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 a60c8c9..ff758e8 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 0000000..cf14fa6
--- /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
-- 
GitLab