aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGES8
-rw-r--r--doc/build/content/defs.txt7
-rw-r--r--lib/mako/codegen.py70
-rw-r--r--lib/mako/runtime.py45
-rw-r--r--lib/mako/template.py1
-rw-r--r--test/call.py97
6 files changed, 180 insertions, 48 deletions
diff --git a/CHANGES b/CHANGES
index 3d22ea3..b7406c6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2,7 +2,13 @@
- fix to module_directory path generation when the path is "./"
[ticket:34]
- TGPlugin passes options to string-based templates [ticket:35]
-
+- added an explicit stack frame step to template runtime, which
+ allows much simpler and hopefully bug-free tracking of 'caller',
+ fixes #28
+- if plain Python defs are used with <%call>, a decorator
+ @runtime.supports_callable exists to ensure that the "caller"
+ stack is properly handled for the def.
+
0.1.5
- AST expression generation - added in just about everything
expression-wise from the AST module [ticket:26]
diff --git a/doc/build/content/defs.txt b/doc/build/content/defs.txt
index 57e2bcd..43e84ac 100644
--- a/doc/build/content/defs.txt
+++ b/doc/build/content/defs.txt
@@ -106,7 +106,9 @@ Assigning to a name inside of a def declares that name as local to the scope of
### Calling a def with embedded content and/or other defs {@name=defswithcontent}
-A flip-side to def within def is a def call with content. This is where you call a def, and at the same time declare a block of content (or multiple blocks) that can be used by the def being called. To achieve this, the target def is invoked using the `<%call>` tag instead of the normal `${}` syntax. The target def then gets a variable `caller` placed in its context which contains a **namespace** containing the body and other defs defined within the `<%call>` tag. The body itself is referenced by the method `body()`:
+A flip-side to def within def is a def call with content. This is where you call a def, and at the same time declare a block of content (or multiple blocks) that can be used by the def being called. The main point of such a call is to create custom, nestable tags, just like any other template language's custom-tag creation system - where the external tag controls the execution of the nested tags and can communicate state to them. Only with Mako, you don't have to use any external Python modules, you can define arbitrarily nestable tags right in your templates.
+
+To achieve this, the target def is invoked using the `<%call>` tag instead of the normal `${}` syntax. The target def then gets a variable `caller` placed in its context which contains a **namespace** containing the body and other defs defined within the `<%call>` tag. The body itself is referenced by the method `body()`:
<%def name="buildtable()">
<table>
@@ -242,3 +244,6 @@ The above layout would produce (whitespace formatted):
this is the body
</div>
</div>
+
+The number of things you can do with `<%call>` 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 or even plain Python callables which are called via `<%call>`.
+
diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py
index 52ded80..2d5984e 100644
--- a/lib/mako/codegen.py
+++ b/lib/mako/codegen.py
@@ -150,10 +150,13 @@ class _GenerateRenderMethod(object):
"""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)))
+ self.printer.writelines(
+ "def %s(%s):" % (name, ','.join(args)),
+ "context.caller_stack.push_frame()",
+ "try:"
+ )
if buffered or filtered or cached:
self.printer.writeline("context.push_buffer()")
- self.printer.writeline("try:")
self.identifier_stack.append(self.compiler.identifiers.branch(self.node))
if not self.in_def and '**pageargs' in args:
@@ -182,20 +185,22 @@ class _GenerateRenderMethod(object):
def write_inherit(self, node):
"""write the module-level inheritance-determination callable."""
- self.printer.writeline("def _mako_inherit(template, context):")
- self.printer.writeline("_mako_generate_namespaces(context)")
- self.printer.writeline("return runtime._inherit_from(context, %s, _template_uri)" % (node.parsed_attributes['file']))
- self.printer.writeline(None)
+ 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)]",
+ "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):")
@@ -312,10 +317,13 @@ class _GenerateRenderMethod(object):
filtered = len(node.filter_args.args) > 0
buffered = eval(node.attributes.get('buffered', 'False'))
cached = eval(node.attributes.get('cached', 'False'))
+ self.printer.writelines(
+ "context.caller_stack.push_frame()",
+ "try:"
+ )
if buffered or filtered or cached:
self.printer.writelines(
"context.push_buffer()",
- "try:"
)
identifiers = identifiers.branch(node, nested=nested)
@@ -332,7 +340,7 @@ class _GenerateRenderMethod(object):
if cached:
self.write_cache_decorator(node, node.name, False, identifiers, inline=True)
- def write_def_finish(self, node, buffered, filtered, cached):
+ 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.
@@ -340,9 +348,19 @@ class _GenerateRenderMethod(object):
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:
- self.printer.writeline("finally:")
- self.printer.writeline("_buf = context.pop_buffer()")
+ self.printer.writelines(
+ "finally:",
+ "_buf = context.pop_buffer()"
+ )
+ if callstack:
+ self.printer.writeline("context.caller_stack.pop_frame()")
s = "_buf.getvalue()"
if filtered:
s = self.create_filter_callable(node.filter_args.args, s, False)
@@ -352,8 +370,10 @@ class _GenerateRenderMethod(object):
if buffered or cached:
self.printer.writeline("return %s" % s)
else:
- self.printer.writeline("context.write(%s)" % s)
- self.printer.writeline("return ''")
+ self.printer.writelines(
+ "context.write(%s)" % s,
+ "return ''"
+ )
def write_cache_decorator(self, node_or_pagetag, name, buffered, identifiers, inline=False):
"""write a post-function decorator to replace a rendering callable with a cached version of itself."""
@@ -493,8 +513,9 @@ 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)
body_identifiers.add_declared('caller')
- callable_identifiers.add_declared('caller')
self.identifier_stack.append(body_identifiers)
class DefVisitor(object):
@@ -504,6 +525,7 @@ class _GenerateRenderMethod(object):
# remove defs that are within the <%call> from the "closuredefs" defined
# in the body, so they dont render twice
del body_identifiers.closuredefs[node.name]
+
vis = DefVisitor()
for n in node.nodes:
n.accept_visitor(vis)
@@ -524,7 +546,7 @@ class _GenerateRenderMethod(object):
n.accept_visitor(self)
self.identifier_stack.pop()
- self.write_def_finish(node, buffered, False, False)
+ self.write_def_finish(node, buffered, False, False, callstack=False)
self.printer.writelines(
None,
"return [%s]" % (','.join(export)),
@@ -532,16 +554,16 @@ class _GenerateRenderMethod(object):
)
self.printer.writelines(
- # push on global "caller" to be picked up by the next ccall
- "caller = context['caller']._get_actual_caller()",
- "context.push_caller(runtime.Namespace('caller', context, callables=ccall(runtime._StackFacade(context, caller))))",
+ # 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))",
"try:")
self.write_source_comment(node)
self.printer.writelines(
"context.write(unicode(%s))" % node.attributes['expr'],
"finally:",
- # pop it off
- "context.pop_caller()",
+ "context.caller_stack.nextcaller = None",
None
)
diff --git a/lib/mako/runtime.py b/lib/mako/runtime.py
index 3efa585..f3cb384 100644
--- a/lib/mako/runtime.py
+++ b/lib/mako/runtime.py
@@ -22,8 +22,7 @@ class Context(object):
data['capture'] = lambda x, *args, **kwargs: capture(self, x, *args, **kwargs)
# "caller" stack used by def calls with content
- self.caller_stack = [UNDEFINED]
- data['caller'] = _StackFacade(self, None)
+ self.caller_stack = data['caller'] = CallerStack()
lookup = property(lambda self:self._with_template.lookup)
kwargs = property(lambda self:self._kwargs.copy())
def push_caller(self, caller):
@@ -70,28 +69,20 @@ class Context(object):
x.pop('next', None)
return c
-class _StackFacade(object):
- def __init__(self, context, local):
- self.__stack = context.caller_stack
- self.__local = local
+class CallerStack(list):
+ def __init__(self):
+ self.nextcaller = None
def __nonzero__(self):
- return self._get_actual_caller() and True or False
- def _get_actual_caller(self):
- caller = self.__stack[-1]
- if caller is None:
- return self.__local
- else:
- return caller
+ return self._get_caller() and True or False
+ def _get_caller(self):
+ return self.nextcaller or self[-1]
def __getattr__(self, key):
- caller = self._get_actual_caller()
- callable_ = getattr(caller, key)
- def call_wno_caller(*args, **kwargs):
- try:
- self.__stack.append(None)
- return callable_(*args, **kwargs)
- finally:
- self.__stack.pop()
- return call_wno_caller
+ return getattr(self._get_caller(), key)
+ def push_frame(self):
+ self.append(self.nextcaller or None)
+ self.nextcaller = None
+ def pop_frame(self):
+ self.pop()
class Undefined(object):
@@ -212,6 +203,16 @@ class Namespace(object):
return getattr(self.inherits, key)
raise exceptions.RuntimeException("Namespace '%s' has no member '%s'" % (self.name, key))
+def supports_caller(func):
+ """apply a caller_stack compatibility decorator to a plain Python function."""
+ def wrap_stackframe(context, *args, **kwargs):
+ context.caller_stack.push_frame()
+ try:
+ return func(context, *args, **kwargs)
+ finally:
+ context.caller_stack.pop_frame()
+ return wrap_stackframe
+
def capture(context, callable_, *args, **kwargs):
"""execute the given template def, capturing the output into a buffer."""
if not callable(callable_):
diff --git a/lib/mako/template.py b/lib/mako/template.py
index 6dc2603..028d11a 100644
--- a/lib/mako/template.py
+++ b/lib/mako/template.py
@@ -178,6 +178,7 @@ def _compile_text(template, text, filename):
identifier = template.module_id
node = Lexer(text, filename, input_encoding=template.input_encoding, preprocessor=template.preprocessor).parse()
source = codegen.compile(node, template.uri, filename, default_filters=template.default_filters, buffer_filters=template.buffer_filters, imports=template.imports)
+ #print source
cid = identifier
module = imp.new_module(cid)
code = compile(source, cid, 'exec')
diff --git a/test/call.py b/test/call.py
index 41fc27c..3dfdbe3 100644
--- a/test/call.py
+++ b/test/call.py
@@ -78,6 +78,7 @@ class CallTest(unittest.TestCase):
</%call>
""")
+ #print t.code
assert result_lines(t.render()) == [
"OUTER BEGIN",
"INNER BEGIN",
@@ -85,6 +86,46 @@ class CallTest(unittest.TestCase):
"INNER END",
"OUTER END",
]
+
+ def test_conditional_call(self):
+ """test that 'caller' is non-None only if the immediate <%def> was called via <%call>"""
+
+ t = Template("""
+ <%def name="a()">
+ % if caller:
+ ${ caller.body() } \\
+ % endif
+ AAA
+ ${ b() }
+ </%def>
+
+ <%def name="b()">
+ % if caller:
+ ${ caller.body() } \\
+ % endif
+ BBB
+ ${ c() }
+ </%def>
+
+ <%def name="c()">
+ % if caller:
+ ${ caller.body() } \\
+ % endif
+ CCC
+ </%def>
+
+ <%call expr="a()">
+ CALL
+ </%call>
+
+ """)
+ assert result_lines(t.render()) == [
+ "CALL",
+ "AAA",
+ "BBB",
+ "CCC"
+ ]
+
def test_chained_call(self):
"""test %calls that are chained through their targets"""
t = Template("""
@@ -222,6 +263,62 @@ class CallTest(unittest.TestCase):
""")
assert result_lines(t.render()) == ['this is a', 'this is b', 'this is c:', "this is the body in b's call"]
+ def test_regular_defs(self):
+ t = Template("""
+ <%!
+ @runtime.supports_caller
+ def a(context):
+ context.write("this is a")
+ if context['caller']:
+ context['caller'].body()
+ context.write("a is done")
+ return ''
+ %>
+
+ <%def name="b()">
+ this is b
+ our body: ${caller.body()}
+ ${a(context)}
+ </%def>
+ test 1
+ <%call expr="a(context)">
+ this is the body
+ </%call>
+ test 2
+ <%call expr="b()">
+ this is the body
+ </%call>
+ test 3
+ <%call expr="b()">
+ this is the body
+ <%call expr="b()">
+ this is the nested body
+ </%call>
+ </%call>
+
+
+ """)
+ #print t.code
+ assert result_lines(t.render()) == [
+ "test 1",
+ "this is a",
+ "this is the body",
+ "a is done",
+ "test 2",
+ "this is b",
+ "our body:",
+ "this is the body",
+ "this is aa is done",
+ "test 3",
+ "this is b",
+ "our body:",
+ "this is the body",
+ "this is b",
+ "our body:",
+ "this is the nested body",
+ "this is aa is done",
+ "this is aa is done"
+ ]
def test_call_in_nested_2(self):
t = Template("""