diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py index 8bfb1b985ea04a7e4cfba53163b622c796a2f3f9..537926a1aa88ed3372f36171cdf37d64e2439c7f 100644 --- a/lib/mako/codegen.py +++ b/lib/mako/codegen.py @@ -1,9 +1,12 @@ """provides the Compiler object for generating module source code.""" from StringIO import StringIO +import time from mako.pygen import PythonPrinter from mako import util, ast +MAGIC_NUMBER = 1 + class Compiler(object): def __init__(self, node, filename=None): self.node = node @@ -17,32 +20,36 @@ class Compiler(object): f = FindPyDecls() self.node.accept_visitor(f) - components = [] - class FindTopLevelComponents(object): - def visitComponentTag(self, node): - components.append(node) - self.node.accept_visitor(FindTopLevelComponents()) + components = _find_top_level_components(self.node) (module_declared, module_undeclared) = (util.Set(), util.Set()) buf = StringIO() printer = PythonPrinter(buf) + # module-level names, python code + printer.writeline("from mako import runtime") + printer.writeline("_magic_number = %s" % repr(MAGIC_NUMBER)) + printer.writeline("_modified_time = %s" % repr(time.time())) printer.writeline("_template_filename=%s" % repr(self.filename)) + buf.write("\n\n") for n in module_code: - (module_declared, module_undeclared) = self._get_declared([n], module_declared, module_undeclared) + (module_declared, module_undeclared) = _find_declared_identifiers([n], module_declared, module_undeclared) printer.writeline("# SOURCE LINE %d" % n.lineno, is_comment=True) printer.write_indented_block(n.text) - (declared, undeclared) = self._get_declared(self.node.nodes, module_declared) - self.node.accept_visitor(_GenerateRenderMethod(printer, undeclared)) + # print main render() method + (declared, undeclared) = _find_declared_identifiers(self.node.nodes, module_declared) + self.node.accept_visitor(_GenerateRenderMethod(printer, declared, undeclared, components)) printer.writeline(None) buf.write("\n\n") + # print render() for each top-level component for node in components: declared = util.Set(node.declared_identifiers()).union(module_declared) - (declared, undeclared) = self._get_declared(node.nodes, declared) - render = _GenerateRenderMethod(printer, undeclared, name="render_%s" % node.name, args=node.function_decl.get_argument_expressions()) + (declared, undeclared) = _find_declared_identifiers(node.nodes, declared) + local_components = _find_top_level_components(node.nodes) + render = _GenerateRenderMethod(printer, declared, undeclared, components + local_components, name="render_%s" % node.name, args=node.function_decl.get_argument_expressions()) for n in node.nodes: n.accept_visitor(render) printer.writeline("return ''") @@ -51,44 +58,9 @@ class Compiler(object): return buf.getvalue() - def _get_declared(self, nodes, declared=None, undeclared=None): - if declared is None: - declared = util.Set() - else: - declared = util.Set(declared) - if undeclared is None: - undeclared = util.Set() - else: - undeclared = util.Set(undeclared) - def check_declared(node): - for ident in node.declared_identifiers(): - declared.add(ident) - for ident in node.undeclared_identifiers(): - if ident != 'context' and ident not in declared: - undeclared.add(ident) - class FindUndeclared(object): - def visitExpression(self, node): - check_declared(node) - def visitControlLine(self, node): - check_declared(node) - def visitCode(self, node): - if not node.ismodule: - check_declared(node) - def visitComponentTag(self, node): - check_declared(node) - def visitIncludeTag(self, node): - # TODO: expressions for attributes - pass - def visitNamespaceTag(self, node): - # TODO: expressions for attributes - pass - fd = FindUndeclared() - for n in nodes: - n.accept_visitor(FindUndeclared()) - return (declared, undeclared) - + class _GenerateRenderMethod(object): - def __init__(self, printer, undeclared, name='render', in_component=False, args=None): + def __init__(self, printer, declared, undeclared, components, name='render', in_component=False, args=None): self.printer = printer self.in_component = in_component self.last_source_line = -1 @@ -97,43 +69,137 @@ class _GenerateRenderMethod(object): else: args = [a for a in ['context'] + args] printer.writeline("def %s(%s):" % (name, ','.join(args))) + + self.write_variable_declares(declared, undeclared, components) + + def write_variable_declares(self, declared, undeclared, components): + comp_idents = dict([(c.name, c) for c in components]) for ident in undeclared: - printer.writeline("%s = context.get(%s, None)" % (ident, repr(ident))) - def writeSourceComment(self, node): + if ident in comp_idents: + comp = comp_idents[ident] + if comp.is_root(): + self.write_component_decl(comp) + else: + self.write_inline_component(comp, declared.union(undeclared), None) + else: + self.printer.writeline("%s = context.get(%s, None)" % (ident, repr(ident))) + + def write_source_comment(self, node): if self.last_source_line != node.lineno: self.printer.writeline("# SOURCE LINE %d" % node.lineno, is_comment=True) self.last_source_line = node.lineno + + def write_component_decl(self, node): + funcname = node.function_decl.funcname + namedecls = node.function_decl.get_argument_expressions() + nameargs = node.function_decl.get_argument_expressions(include_defaults=False) + 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_component(self, node, declared, undeclared): + namedecls = node.function_decl.get_argument_expressions() + self.printer.writeline("def %s(%s):" % (node.name, ",".join(namedecls))) + components = _find_top_level_components(node.nodes) + (declared, undeclared) = _find_declared_identifiers(node.nodes, declared, undeclared) + self.write_variable_declares(declared, undeclared, components) + for n in node.nodes: + n.accept_visitor(self) + self.printer.writeline("return ''") + self.printer.writeline(None) + def visitExpression(self, node): - self.writeSourceComment(node) + self.write_source_comment(node) self.printer.writeline("context.write(unicode(%s))" % node.text) def visitControlLine(self, node): if node.isend: self.printer.writeline(None) else: - self.writeSourceComment(node) + self.write_source_comment(node) self.printer.writeline(node.text) def visitText(self, node): - self.writeSourceComment(node) + self.write_source_comment(node) self.printer.writeline("context.write(%s)" % repr(node.content)) def visitCode(self, node): if not node.ismodule: - self.writeSourceComment(node) + self.write_source_comment(node) self.printer.write_indented_block(node.text) def visitIncludeTag(self, node): - self.writeSourceComment(node) + self.write_source_comment(node) self.printer.writeline("context.include_file(%s, import=%s)" % (repr(node.attributes['file']), repr(node.attributes.get('import', False)))) def visitNamespaceTag(self, node): - pass - def visitComponentTag(self, node): - self.writeSourceComment(node) - funcname = node.function_decl.funcname - namedecls = node.function_decl.get_argument_expressions() - nameargs = node.function_decl.get_argument_expressions(include_defaults=False) - nameargs.insert(0, 'context') - self.printer.writeline("def %s(%s):" % (funcname, ",".join(namedecls))) - self.printer.writeline("return render_%s(%s)" % (funcname, ",".join(nameargs))) + self.write_source_comment(node) + self.printer.writeline("def make_namespace():") + export = [] + class NSComponentVisitor(object): + def visitComponentTag(s, node): + self.write_inline_component(node, None, None) + export.append(node.name) + vis = NSComponentVisitor() + for n in node.nodes: + n.accept_visitor(vis) + self.printer.writeline("return %s" % (repr(export))) + self.printer.writeline(None) + self.printer.writeline("class %sNamespace(runtime.Namespace):" % node.name) + self.printer.writeline("def __getattr__(self, key):") + self.printer.writeline("return self.contextual_callable(context, key)") + self.printer.writeline(None) self.printer.writeline(None) + self.printer.writeline("%s = %sNamespace(%s, callables=make_namespace())" % (node.name, node.name)) + + def visitComponentTag(self, node): + pass def visitCallTag(self, node): pass def visitInheritTag(self, node): pass + +def _find_top_level_components(nodes): + components = [] + class FindTopLevelComponents(object): + def visitComponentTag(self, node): + components.append(node) + ftl = FindTopLevelComponents() + if isinstance(nodes, list): + for n in nodes: + n.accept_visitor(ftl) + else: + nodes.accept_visitor(ftl) + return components + +def _find_declared_identifiers(nodes, declared=None, undeclared=None): + if declared is None: + declared = util.Set() + else: + declared = util.Set(declared) + if undeclared is None: + undeclared = util.Set() + else: + undeclared = util.Set(undeclared) + def check_declared(node): + for ident in node.declared_identifiers(): + declared.add(ident) + for ident in node.undeclared_identifiers(): + if ident != 'context' and ident not in declared: + undeclared.add(ident) + class FindUndeclared(object): + def visitExpression(self, node): + check_declared(node) + def visitControlLine(self, node): + check_declared(node) + def visitCode(self, node): + if not node.ismodule: + check_declared(node) + def visitComponentTag(self, node): + pass + #check_declared(node) + def visitIncludeTag(self, node): + # TODO: expressions for attributes + pass + def visitNamespaceTag(self, node): + check_declared(node) + fd = FindUndeclared() + for n in nodes: + n.accept_visitor(FindUndeclared()) + return (declared, undeclared) diff --git a/lib/mako/context.py b/lib/mako/context.py deleted file mode 100644 index a9d8ece559cf8bc66a5f5dade5125247be9a20f5..0000000000000000000000000000000000000000 --- a/lib/mako/context.py +++ /dev/null @@ -1,16 +0,0 @@ -"""provides the Context class, the runtime namespace for templates.""" - - -class Context(object): - """provides runtime namespace and output buffer for templates.""" - def __init__(self, buffer, **data): - self.buffer = buffer - self.data = data - # the Template instance currently rendering with this context. - self.with_template = None - def __getitem__(self, key): - return self.data[key] - def get(self, key, default=None): - return self.data.get(key, default) - def write(self, string): - self.buffer.write(string) \ No newline at end of file diff --git a/lib/mako/lexer.py b/lib/mako/lexer.py index 81abdf98b1945f6975feb434d9dbecdd0532c268..2dfa5d1f3d4902e2e23f54e6d18ec295fedff664 100644 --- a/lib/mako/lexer.py +++ b/lib/mako/lexer.py @@ -93,7 +93,23 @@ class Lexer(object): return self.template def match_tag_start(self): - match = self.match(r'''\<%(\w+)\s+(.+?["'])?\s*(/)?>''', re.I | re.S ) + match = self.match(r''' + \<% # opening tag + + (\w+) # keyword + + \s+ # some space + + ((?:\w+|=|".*?"|'.*?')*) # attrname, = sign, string expression + + \s* # more whitespace + + (/)?> # closing + + ''', + + re.I | re.S | re.X) + if match: (keyword, attr, isend) = (match.group(1).lower(), match.group(2), match.group(3)) self.keyword = keyword @@ -102,7 +118,6 @@ class Lexer(object): for att in re.findall(r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr): (key, val1, val2) = att attributes[key] = val1 or val2 - self.append_node(parsetree.Tag, keyword, attributes) if isend: self.tag.pop() diff --git a/lib/mako/parsetree.py b/lib/mako/parsetree.py index 40e78077ae09fc9906d6f0b89646b4408caba8ab..34dbf7778864fb7e86d27e4f96efb17ebb065951 100644 --- a/lib/mako/parsetree.py +++ b/lib/mako/parsetree.py @@ -140,7 +140,7 @@ class _TagMeta(type): return type.__call__(cls, keyword, attributes, **kwargs) class Tag(Node): - """base class for tags. + """abstract base class for tags. <%sometag/> @@ -150,27 +150,76 @@ class Tag(Node): """ __metaclass__ = _TagMeta __keyword__ = None - def __init__(self, keyword, attributes, **kwargs): + def __init__(self, keyword, attributes, expressions, nonexpressions, required, **kwargs): + """construct a new Tag instance. + + this constructor not called directly, and is only called by subclasses. + + keyword - the tag keyword + + attributes - raw dictionary of attribute key/value pairs + + expressions - a util.Set of identifiers that are legal attributes, which can also contain embedded expressions + + nonexpressions - a util.Set of identifiers that are legal attributes, which cannot contain embedded expressions + + **kwargs - other arguments passed to the Node superclass (lineno, pos)""" super(Tag, self).__init__(**kwargs) self.keyword = keyword self.attributes = attributes + self._parse_attributes(expressions, nonexpressions) + missing = [r for r in required if r not in self.parsed_attributes] + if len(missing): + raise exceptions.CompileException("Missing attribute(s): %s" % ",".join([repr(m) for m in missing]), self.lineno, self.pos) self.parent = None self.nodes = [] def is_root(self): return self.parent is None def get_children(self): return self.nodes + def _parse_attributes(self, expressions, nonexpressions): + undeclared_identifiers = util.Set() + self.parsed_attributes = {} + for key in self.attributes: + if key in expressions: + expr = [] + for x in re.split(r'(\${.+?})', self.attributes[key]): + m = re.match(r'^\${(.+?)}$', x) + if m: + code = ast.PythonCode(m.group(1), self.lineno, self.pos) + undeclared_identifiers = undeclared_identifiers.union(code.undeclared_identifiers) + expr.append(m.group(1)) + else: + expr.append(repr(x)) + self.parsed_attributes[key] = " + ".join(expr) + elif key in nonexpressions: + if re.search(r'${.+?}', self.attributes[key]): + raise exceptions.CompileException("Attibute '%s' in tag '%s' does not allow embedded expressions" %(key, self.keyword), self.lineno, self.pos) + self.parsed_attributes[key] = repr(self.attributes[key]) + else: + raise exceptions.CompileException("Invalid attribute for tag '%s': '%s'" %(self.keyword, key), self.lineno, self.pos) def __repr__(self): return "%s(%s, %s, %s, %s)" % (self.__class__.__name__, repr(self.keyword), repr(self.attributes), repr((self.lineno, self.pos)), repr([repr(x) for x in self.nodes])) class IncludeTag(Tag): __keyword__ = 'include' + def __init__(self, keyword, attributes, **kwargs): + super(IncludeTag, self).__init__(keyword, attributes, util.Set(['file', 'import']), util.Set([]), util.Set(['file']), **kwargs) + class NamespaceTag(Tag): __keyword__ = 'namespace' + def __init__(self, keyword, attributes, **kwargs): + super(NamespaceTag, self).__init__(keyword, attributes, util.Set(['file']), util.Set(['name']), util.Set(['name']), **kwargs) + self.name = attributes['name'] + def declared_identifiers(self): + return [self.name] + def undeclared_identifiers(self): + return [] + class ComponentTag(Tag): __keyword__ = 'component' def __init__(self, keyword, attributes, **kwargs): - super(ComponentTag, self).__init__(keyword, attributes, **kwargs) + super(ComponentTag, self).__init__(keyword, attributes, util.Set([]), util.Set(['name']), util.Set(['name']), **kwargs) name = attributes['name'] if re.match(r'^[\w_]+$',name): name = name + "()" @@ -183,9 +232,19 @@ class ComponentTag(Tag): for c in self.function_decl.defaults: res += list(ast.PythonCode(c, self.lineno, self.pos).undeclared_identifiers) return res + class CallTag(Tag): __keyword__ = 'call' + def __init__(self, keyword, attributes, **kwargs): + super(CallTag, self).__init__(keyword, attributes, util.Set([]), util.Set(['expr']), util.Set(['expr']), **kwargs) + class InheritTag(Tag): __keyword__ = 'inherit' + def __init__(self, keyword, attributes, **kwargs): + super(InheritTag, self).__init__(keyword, attributes, util.Set(['file']), util.Set([]), util.Set(['file']), **kwargs) + class PageTag(Tag): - __keyword__ = 'page' \ No newline at end of file + __keyword__ = 'page' + def __init__(self, keyword, attributes, **kwargs): + super(PageTag, self).__init__(keyword, attributes, util.Set([]), util.Set([]), util.Set([]), **kwargs) + \ No newline at end of file diff --git a/lib/mako/runtime.py b/lib/mako/runtime.py new file mode 100644 index 0000000000000000000000000000000000000000..76936c8abb55114eb61a202a6175a479ad7a8655 --- /dev/null +++ b/lib/mako/runtime.py @@ -0,0 +1,46 @@ +"""provides the Context class, the runtime namespace for templates.""" +from mako import exceptions + +class Context(object): + """provides runtime namespace and output buffer for templates.""" + def __init__(self, buffer, **data): + self.buffer = buffer + self.data = data + # the Template instance currently rendering with this context. + self.with_template = None + def __getitem__(self, key): + return self.data[key] + def get(self, key, default=None): + return self.data.get(key, default) + def write(self, string): + self.buffer.write(string) + + +class Namespace(object): + """provides access to collections of rendering methods, which can be local, from other templates, or from imported modules""" + def __init__(self, name, module=None, template=None, callables=None): + self.module = module + self.template = template + self.callables = callables + + def load(self, key): + if self.callables is not None: + try: + return self.callables[key] + except KeyError: + pass + if self.template is not None: + try: + return self.template.get_component(key) + except AttributeError: + pass + if self.module is not None: + try: + return getattr(self.module, key) + except AttributeError: + pass + raise exceptions.RuntimeException("Namespace '%s' has no member '%s'" % (self.name, key)) + + def contextual_callable(self, context, key): + return lambda context, *args, **kwargs:self.load(key)(context, *args, **kwargs) + \ No newline at end of file diff --git a/lib/mako/template.py b/lib/mako/template.py index 9df070f4cb52e42306b246cb8eda46d38cf44a49..08d925f575d0fc3853174a452dd7709a71ba9a34 100644 --- a/lib/mako/template.py +++ b/lib/mako/template.py @@ -3,7 +3,7 @@ as well as template runtime operations.""" from mako.lexer import Lexer from mako.codegen import Compiler -from mako.context import Context +from mako.runtime import Context import imp, time, inspect, weakref, sys from StringIO import StringIO @@ -80,11 +80,11 @@ class ComponentTemplate(Template): def _compile_text(text, identifier, filename): node = Lexer(text).parse() source = Compiler(node).render() + #print source cid = identifier module = imp.new_module(cid) code = compile(source, filename or cid, 'exec') exec code in module.__dict__, module.__dict__ - module._modified_time = time.time() return (source, module) def _render(template, callable_, *args, **data): diff --git a/test/lexer.py b/test/lexer.py index 6bec2482f8e8ed69922854a9d88916d9041416ea..385111078d19dd4f05e2e7f1154592a6d8443e61 100644 --- a/test/lexer.py +++ b/test/lexer.py @@ -14,8 +14,7 @@ class LexerTest(unittest.TestCase): and some more text. """ node = Lexer(template).parse() - #print repr(nodes) - assert repr(node) == r"""TemplateNode({}, [Text('\n<b>Hello world</b>\n ', (1, 1)), ComponentTag('component', {'name': '"foo"'}, (3, 9), ["Text('\\n this is a component.\\n ', (3, 32))"]), Text('\n \n and some more text.\n', (5, 22))])""" + assert repr(node) == r"""TemplateNode({}, [Text('\n<b>Hello world</b>\n ', (1, 1)), ComponentTag('component', {'name': 'foo'}, (3, 9), ["Text('\\n this is a component.\\n ', (3, 32))"]), Text('\n \n and some more text.\n', (5, 22))])""" def test_unclosed_tag(self): template = """ @@ -29,6 +28,28 @@ class LexerTest(unittest.TestCase): except exceptions.SyntaxException, e: assert str(e) == "Unclosed tag: <%component> at line: 5 char: 9" + def test_nonexistent_tag(self): + template = """ + <%lala x="5"/> + """ + try: + node = Lexer(template).parse() + assert False + except exceptions.CompileException, e: + assert str(e) == "No such tag: 'lala' at line: 2 char: 13" + + def test_component_syntax(self): + template = """ + <%component lala> + hi + </%component> +""" + try: + node = Lexer(template).parse() + assert False + except exceptions.CompileException, e: + assert str(e) == "Missing attribute(s): 'name' at line: 2 char: 9" + def test_expr_in_attribute(self): """test some slightly trickier expressions. @@ -39,7 +60,8 @@ class LexerTest(unittest.TestCase): """ nodes = Lexer(template).parse() #print nodes - assert repr(nodes) == r"""TemplateNode({}, [Text('\n ', (1, 1)), CallTag('call', {'expr': '"foo>bar and \'lala\' or \'hoho\'"'}, (2, 13), []), Text('\n ', (2, 57)), CallTag('call', {'expr': '\'foo<bar and hoho>lala and "x" + "y"\''}, (3, 13), []), Text('\n ', (3, 64))])""" + assert repr(nodes) == r"""TemplateNode({}, [Text('\n ', (1, 1)), CallTag('call', {'expr': "foo>bar and 'lala' or 'hoho'"}, (2, 13), []), Text('\n ', (2, 57)), CallTag('call', {'expr': 'foo<bar and hoho>lala and "x" + "y"'}, (3, 13), []), Text('\n ', (3, 64))])""" + def test_nesting(self): template = """ @@ -52,7 +74,7 @@ class LexerTest(unittest.TestCase): """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text('\n \n ', (1, 1)), NamespaceTag('namespace', {'name': '"ns"'}, (3, 9), ["Text('\\n ', (3, 31))", 'ComponentTag(\'component\', {\'name\': \'"lala(hi, there)"\'}, (4, 13), ["Text(\'\\\\n \', (4, 48))", \'CallTag(\\\'call\\\', {\\\'expr\\\': \\\'"something()"\\\'}, (5, 17), [])\', "Text(\'\\\\n \', (5, 44))"])', "Text('\\n ', (6, 26))"]), Text('\n \n ', (7, 22))])""" + assert repr(nodes) == r"""TemplateNode({}, [Text('\n \n ', (1, 1)), NamespaceTag('namespace', {'name': 'ns'}, (3, 9), ["Text('\\n ', (3, 31))", 'ComponentTag(\'component\', {\'name\': \'lala(hi, there)\'}, (4, 13), ["Text(\'\\\\n \', (4, 48))", "CallTag(\'call\', {\'expr\': \'something()\'}, (5, 17), [])", "Text(\'\\\\n \', (5, 44))"])', "Text('\\n ', (6, 26))"]), Text('\n \n ', (7, 22))])""" def test_code(self): template = """ @@ -107,8 +129,7 @@ class LexerTest(unittest.TestCase): ${hi()} """ nodes = Lexer(template).parse() - #print nodes - assert repr(nodes) == r"""TemplateNode({}, [Text('\n this is some ', (1, 1)), Expression('text', [], (2, 22)), Text(' and this is ', (2, 29)), Expression('textwith ', ['escapes', 'moreescapes'], (2, 42)), Text('\n ', (2, 76)), ComponentTag('component', {'name': '"hi"'}, (3, 9), ["Text('\\n give me ', (3, 31))", "Expression('foo()', [], (4, 21))", "Text(' and ', (4, 29))", "Expression('bar()', [], (4, 34))", "Text('\\n ', (4, 42))"]), Text('\n ', (5, 22)), Expression('hi()', [], (6, 9)), Text('\n', (6, 16))])""" + assert repr(nodes) == r"""TemplateNode({}, [Text('\n this is some ', (1, 1)), Expression('text', [], (2, 22)), Text(' and this is ', (2, 29)), Expression('textwith ', ['escapes', 'moreescapes'], (2, 42)), Text('\n ', (2, 76)), ComponentTag('component', {'name': 'hi'}, (3, 9), ["Text('\\n give me ', (3, 31))", "Expression('foo()', [], (4, 21))", "Text(' and ', (4, 29))", "Expression('bar()', [], (4, 34))", "Text('\\n ', (4, 42))"]), Text('\n ', (5, 22)), Expression('hi()', [], (6, 9)), Text('\n', (6, 16))])""" def test_control_lines(self): template = """ diff --git a/test/template.py b/test/template.py new file mode 100644 index 0000000000000000000000000000000000000000..167557ede532b5af04bac9ec64655408efc131dc --- /dev/null +++ b/test/template.py @@ -0,0 +1,147 @@ +from mako.template import Template +import unittest + + +class ComponentTest(unittest.TestCase): + def test_component_noargs(self): + template = Template(""" + + ${mycomp()} + + <%component name="mycomp"> + hello mycomp ${variable} + </%component> + + """) + assert template.render(variable='hi').strip() == """hello mycomp hi""" + + def test_component_blankargs(self): + template = Template(""" + <%component name="mycomp()"> + hello mycomp ${variable} + </%component> + + ${mycomp()}""") + assert template.render(variable='hi').strip() == """hello mycomp hi""" + + def test_component_args(self): + template = Template(""" + <%component name="mycomp(a, b)"> + hello mycomp ${variable}, ${a}, ${b} + </%component> + + ${mycomp(5, 6)}""") + assert template.render(variable='hi', a=5, b=6).strip() == """hello mycomp hi, 5, 6""" + + def test_inter_component(self): + """test components calling each other""" + template = Template(""" +${b()} + +<%component name="a">\ +im a +</%component> + +<%component name="b"> +im b +and heres a: ${a()} +</%component> + +<%component name="c"> +im c +</%component> + """) + # check that "a" is declared in "b", but not in "c" + assert "a" not in template.module.render_c.func_code.co_varnames + assert "a" in template.module.render_b.func_code.co_varnames + + # then test output + assert template.render().strip() == "im b\nand heres a: im a" + + def test_nested_component(self): + template = """ + + ${hi()} + + <%component name="hi"> + hey, im hi. + and heres ${foo()}, ${bar()} + + <%component name="foo"> + this is foo + </%component> + + <%component name="bar"> + this is bar + </%component> + </%component> +""" + t = Template(template) + #print t.code + print t.render() + + def test_nested_nested_component(self): + template = """ + + ${a()} + <%component name="a"> + <%component name="b1"> + </%component> + <%component name="b2"> + a_b2 ${c1()} + <%component name="c1"> + a_b2_c1 + </%component> + </%component> + <%component name="b3"> + a_b3 ${c1()} + <%component name="c1"> + a_b3_c1 heres x: ${x} + <% + y = 7 + %> + y is ${y} + </%component> + <%component name="c2"> + a_b3_c2 + y is ${y} + c1 is ${c1()} + </%component> + ${c2()} + </%component> + + ${b1()} ${b2()} ${b3()} + </%component> +""" + + t = Template(template) + #print t.code + print t.render(x=5) + +class ExceptionTest(unittest.TestCase): + def test_raise(self): + template = Template(""" + <% + raise Exception("this is a test") + %> + """, format_exceptions=False) + try: + template.render() + assert False + except Exception, e: + assert str(e) == "this is a test" + def test_handler(self): + def handle(context, error): + context.write("error message is " + str(error)) + return True + + template = Template(""" + <% + raise Exception("this is a test") + %> + """, error_handler=handle) + assert template.render().strip() == """error message is this is a test""" + + +if __name__ == '__main__': + unittest.main()