Mercurial > genshi > mirror
changeset 81:d60486018004 trunk
Template expressions are now compiled to Python bytecode.
author | cmlenz |
---|---|
date | Sat, 15 Jul 2006 11:29:25 +0000 |
parents | e0957965553f |
children | 5ca4be55ad0b |
files | examples/bench/kid/template.kid examples/bench/markup/base.html examples/bench/markup/template.html examples/bench/run.py markup/eval.py markup/template.py markup/tests/eval.py markup/tests/template.py |
diffstat | 8 files changed, 293 insertions(+), 337 deletions(-) [+] |
line wrap: on
line diff
--- a/examples/bench/kid/template.kid +++ b/examples/bench/kid/template.kid @@ -16,7 +16,7 @@ <h2>Loop</h2> <ul py:if="items"> <li py:for="idx, item in enumerate(items)" py:content="item" - class="${idx == len(items) and 'last' or None}" /> + class="${idx + 1 == len(items) and 'last' or None}" /> </ul> </body> </html>
--- a/examples/bench/markup/base.html +++ b/examples/bench/markup/base.html @@ -6,12 +6,12 @@ Hello, ${name}! </p> - <py:match path="body"> + <body py:match="body"> <div id="header"> <h1>${title}</h1> </div> ${select('*')} <div id="footer" /> - </py:match> + </body> </html>
--- a/examples/bench/markup/template.html +++ b/examples/bench/markup/template.html @@ -17,7 +17,7 @@ <h2>Loop</h2> <ul py:if="items"> <li py:for="idx, item in enumerate(items)" py:content="item" - class="${idx == len(items) and 'last' or None}" /> + class="${idx + 1 == len(items) and 'last' or None}" /> </ul> </body>
--- a/examples/bench/run.py +++ b/examples/bench/run.py @@ -6,7 +6,7 @@ __all__ = ['clearsilver', 'django', 'kid', 'markup', 'simpletal'] -def markup(dirname): +def markup(dirname, verbose=False): from markup.template import Context, TemplateLoader loader = TemplateLoader([dirname], auto_reload=False) template = loader.load('template.html') @@ -14,9 +14,12 @@ ctxt = Context(title='Just a test', user='joe', items=['Number %d' % num for num in range(1, 15)]) return template.generate(ctxt).render('html') + + if verbose: + print render() return render -def cheetah(dirname): +def cheetah(dirname, verbose=False): # FIXME: infinite recursion somewhere... WTF? from Cheetah.Template import Template class MyTemplate(Template): @@ -29,9 +32,12 @@ searchList=[{'title': 'Just a test', 'user': 'joe', 'items': [u'Number %d' % num for num in range(1, 15)]}]) return template.respond() + + if verbose: + print render() return render -def clearsilver(dirname): +def clearsilver(dirname, verbose=False): import neo_cgi neo_cgi.update() import neo_util @@ -46,9 +52,12 @@ cs = neo_cs.CS(hdf) cs.parseFile('template.cs') return cs.render() + + if verbose: + print render() return render -def django(dirname): +def django(dirname, verbose=False): from django.conf import settings settings.configure(TEMPLATE_DIRS=[os.path.join(dirname, 'templates')]) from django import template, templatetags @@ -60,9 +69,12 @@ data = {'title': 'Just a test', 'user': 'joe', 'items': ['Number %d' % num for num in range(1, 15)]} return tmpl.render(template.Context(data)) + + if verbose: + print render() return render -def kid(dirname): +def kid(dirname, verbose=False): import kid kid.path = kid.TemplatePath([dirname]) template = kid.Template(file='template.kid') @@ -71,17 +83,23 @@ title='Just a test', user='joe', items=['Number %d' % num for num in range(1, 15)]) return template.serialize(output='xhtml') + + if verbose: + print render() return render -def nevow(dirname): +def nevow(dirname, verbose=False): # FIXME: can't figure out the API from nevow.loaders import xmlfile template = xmlfile('template.xml', templateDir=dirname).load() def render(): print template + + if verbose: + print render() return render -def simpletal(dirname): +def simpletal(dirname, verbose=False): from simpletal import simpleTAL, simpleTALES fileobj = open(os.path.join(dirname, 'base.html')) base = simpleTAL.compileHTMLTemplate(fileobj) @@ -98,17 +116,29 @@ buf = StringIO() template.expand(ctxt, buf) return buf.getvalue() + + if verbose: + print render() return render -def run(engines): +def run(engines, verbose=False): basepath = os.path.abspath(os.path.dirname(__file__)) for engine in engines: dirname = os.path.join(basepath, engine) - print '%s:' % engine.capitalize(), - t = timeit.Timer(setup='from __main__ import %s; render = %s("%s")' - % (engine, engine, dirname), + if verbose: + print '%s:' % engine.capitalize() + print '--------------------------------------------------------' + else: + print '%s:' % engine.capitalize(), + t = timeit.Timer(setup='from __main__ import %s; render = %s("%s", %s)' + % (engine, engine, dirname, verbose), stmt='render()') - print '%.2f ms' % (1000 * t.timeit(number=2000) / 2000) + time = t.timeit(number=2000) / 2000 + if verbose: + print '--------------------------------------------------------' + print '%.2f ms' % (1000 * time) + if verbose: + print '--------------------------------------------------------' if __name__ == '__main__': @@ -116,13 +146,15 @@ if not engines: engines = __all__ + verbose = '-v' in sys.argv + if '-p' in sys.argv: import hotshot, hotshot.stats prof = hotshot.Profile("template.prof") - benchtime = prof.runcall(run, engines) + benchtime = prof.runcall(run, engines, verbose=verbose) stats = hotshot.stats.load("template.prof") stats.strip_dirs() stats.sort_stats('time', 'calls') stats.print_stats() else: - run(engines) + run(engines, verbose=verbose)
--- a/markup/eval.py +++ b/markup/eval.py @@ -16,12 +16,7 @@ from __future__ import division import __builtin__ -try: - import _ast # Python 2.5 -except ImportError: - _ast = None - import compiler -import operator +from compiler import parse, pycodegen from markup.core import Stream @@ -34,6 +29,7 @@ >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) >>> Expression('test').evaluate(data) 'Foo' + >>> Expression('items[0]').evaluate(data) 1 >>> Expression('items[-1]').evaluate(data) @@ -46,7 +42,8 @@ >>> Expression('dict.some').evaluate(data) 'thing' - + """ + """ This also works the other way around: item access can be used to access any object attribute (meaning there's no use for `getattr()` in templates): @@ -68,292 +65,210 @@ >>> Expression('len(items)').evaluate(data) 3 """ - __slots__ = ['source', 'ast'] + __slots__ = ['source', 'code'] _visitors = {} - def __init__(self, source): + def __init__(self, source, filename=None, lineno=-1): """Create the expression. @param source: the expression as string """ self.source = source - self.ast = None + + tree = parse(self.source, 'eval') + if isinstance(filename, unicode): + # pycodegen doesn't like unicode in the filename + filename = filename.encode('utf-8', 'replace') + tree.filename = filename or '<string>' + gen = TemplateExpressionCodeGenerator(tree) + if lineno >= 0: + gen.emit('SET_LINENO', lineno) + self.code = gen.getCode() def __repr__(self): return '<Expression "%s">' % self.source - if _ast is None: - - def evaluate(self, data): - """Evaluate the expression against the given data dictionary. - - @param data: a mapping containing the data to evaluate against - @return: the result of the evaluation - """ - if not self.ast: - self.ast = compiler.parse(self.source, 'eval') - return self._visit(self.ast.node, data) - - # AST traversal - - def _visit(self, node, data): - v = self._visitors.get(node.__class__) - if not v: - v = getattr(self, '_visit_%s' % node.__class__.__name__.lower()) - self._visitors[node.__class__] = v - return v(node, data) - - def _visit_expression(self, node, data): - for child in node.getChildNodes(): - return self._visit(child, data) - - # Functions & Accessors - - def _visit_callfunc(self, node, data): - func = self._visit(node.node, data) - if func is None: - return None - args = [self._visit(arg, data) for arg in node.args - if not isinstance(arg, compiler.ast.Keyword)] - kwargs = dict([(arg.name, self._visit(arg.expr, data)) for arg - in node.args if isinstance(arg, compiler.ast.Keyword)]) - return func(*args, **kwargs) - - def _visit_getattr(self, node, data): - obj = self._visit(node.expr, data) - if hasattr(obj, node.attrname): - return getattr(obj, node.attrname) - try: - return obj[node.attrname] - except (KeyError, TypeError): - return None - - def _visit_slice(self, node, data): - obj = self._visit(node.expr, data) - lower = node.lower and self._visit(node.lower, data) or None - upper = node.upper and self._visit(node.upper, data) or None - return obj[lower:upper] - - def _visit_subscript(self, node, data): - obj = self._visit(node.expr, data) - subs = map(lambda sub: self._visit(sub, data), node.subs) - if len(subs) == 1: - subs = subs[0] - try: - return obj[subs] - except (KeyError, IndexError, TypeError): - try: - return getattr(obj, subs) - except (AttributeError, TypeError): - return None - - # Operators - - def _visit_and(self, node, data): - return reduce(lambda x, y: x and y, - [self._visit(n, data) for n in node.nodes]) - - def _visit_or(self, node, data): - return reduce(lambda x, y: x or y, - [self._visit(n, data) for n in node.nodes]) - - def _visit_bitand(self, node, data): - return reduce(operator.and_, - [self._visit(n, data) for n in node.nodes]) - - def _visit_bitor(self, node, data): - return reduce(operator.or_, - [self._visit(n, data) for n in node.nodes]) - - _OP_MAP = {'==': operator.eq, '!=': operator.ne, - '<': operator.lt, '<=': operator.le, - '>': operator.gt, '>=': operator.ge, - 'is': operator.is_, 'is not': operator.is_not, - 'in': lambda x, y: operator.contains(y, x), - 'not in': lambda x, y: not operator.contains(y, x)} - def _visit_compare(self, node, data): - result = self._visit(node.expr, data) - ops = node.ops[:] - ops.reverse() - for op, rval in ops: - result = self._OP_MAP[op](result, self._visit(rval, data)) - return result - - def _visit_add(self, node, data): - return self._visit(node.left, data) + self._visit(node.right, data) - - def _visit_div(self, node, data): - return self._visit(node.left, data) / self._visit(node.right, data) - - def _visit_floordiv(self, node, data): - return self._visit(node.left, data) // self._visit(node.right, data) - - def _visit_mod(self, node, data): - return self._visit(node.left, data) % self._visit(node.right, data) - - def _visit_mul(self, node, data): - return self._visit(node.left, data) * self._visit(node.right, data) - - def _visit_power(self, node, data): - return self._visit(node.left, data) ** self._visit(node.right, data) - - def _visit_sub(self, node, data): - return self._visit(node.left, data) - self._visit(node.right, data) - - def _visit_unaryadd(self, node, data): - return +self._visit(node.expr, data) - - def _visit_unarysub(self, node, data): - return -self._visit(node.expr, data) - - def _visit_not(self, node, data): - return not self._visit(node.expr, data) - - def _visit_invert(self, node, data): - return ~self._visit(node.expr, data) - - # Identifiers & Literals + def evaluate(self, data): + """Evaluate the expression against the given data dictionary. + + @param data: a mapping containing the data to evaluate against + @return: the result of the evaluation + """ + return eval(self.code) - def _visit_name(self, node, data): - val = data.get(node.name) - if val is None: - val = getattr(__builtin__, node.name, None) - return val - - def _visit_const(self, node, data): - return node.value - - def _visit_dict(self, node, data): - return dict([(self._visit(k, data), self._visit(v, data)) - for k, v in node.items]) - - def _visit_tuple(self, node, data): - return tuple([self._visit(n, data) for n in node.nodes]) - - def _visit_list(self, node, data): - return [self._visit(n, data) for n in node.nodes] - - else: - - def evaluate(self, data): - """Evaluate the expression against the given data dictionary. - - @param data: a mapping containing the data to evaluate against - @return: the result of the evaluation - """ - if not self.ast: - self.ast = compile(self.source, '?', 'eval', 0x400) - return self._visit(self.ast, data) - - # AST traversal - - def _visit(self, node, data): - v = self._visitors.get(node.__class__) - if not v: - v = getattr(self, '_visit_%s' % node.__class__.__name__.lower()) - self._visitors[node.__class__] = v - return v(node, data) - - def _visit_expression(self, node, data): - return self._visit(node.body, data) - - # Functions & Accessors - - def _visit_attribute(self, node, data): - obj = self._visit(node.value, data) - if hasattr(obj, node.attr): - return getattr(obj, node.attr) - try: - return obj[node.attr] - except (KeyError, TypeError): - return None - - def _visit_call(self, node, data): - func = self._visit(node.func, data) - if func is None: - return None - args = [self._visit(arg, data) for arg in node.args] - kwargs = dict([(kwarg.arg, self._visit(kwarg.value, data)) - for kwarg in node.keywords]) - return func(*args, **kwargs) - def _visit_subscript(self, node, data): - obj = self._visit(node.value, data) - if isinstance(node.slice, _ast.Slice): - try: - return obj[self._visit(lower, data): - self._visit(upper, data): - self._visit(step, data)] - except (KeyError, IndexError, TypeError): - pass - else: - index = self._visit(node.slice.value, data) - try: - return obj[index] - except (KeyError, IndexError, TypeError): - try: - return getattr(obj, index) - except (AttributeError, TypeError): - pass - return None - - # Operators - - _OP_MAP = {_ast.Add: operator.add, _ast.And: lambda l, r: l and r, - _ast.BitAnd: operator.and_, _ast.BitOr: operator.or_, - _ast.Div: operator.truediv, _ast.Eq: operator.eq, - _ast.FloorDiv: operator.floordiv, _ast.Gt: operator.gt, - _ast.GtE: operator.ge, _ast.Invert: operator.inv, - _ast.In: lambda l, r: operator.contains(r, l), - _ast.Is: operator.is_, _ast.IsNot: operator.is_not, - _ast.Lt: operator.lt, _ast.LtE: operator.le, - _ast.Mod: operator.mod, _ast.Mult: operator.mul, - _ast.Not: operator.not_, _ast.NotEq: operator.ne, - _ast.NotIn: lambda l, r: not operator.contains(r, l), - _ast.Or: lambda l, r: l or r, _ast.Pow: operator.pow, - _ast.Sub: operator.sub, _ast.UAdd: operator.pos, - _ast.USub: operator.neg} - - def _visit_unaryop(self, node, data): - return self._OP_MAP[node.op.__class__](self._visit(node.operand, data)) +class TemplateExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator): - def _visit_binop(self, node, data): - return self._OP_MAP[node.op.__class__](self._visit(node.left, data), - self._visit(node.right, data)) - - def _visit_boolop(self, node, data): - return reduce(self._OP_MAP[node.op.__class__], - [self._visit(n, data) for n in node.values]) - - def _visit_compare(self, node, data): - result = self._visit(node.left, data) - ops = node.ops[:] - ops.reverse() - for op, rval in zip(ops, node.comparators): - result = self._OP_MAP[op.__class__](result, - self._visit(rval, data)) - return result - - # Identifiers & Literals + def visitGetattr(self, node): + """Overridden to fallback to item access if the object doesn't have an + attribute. + + Also, if either method fails, this returns `None` instead of raising an + `AttributeError`. + """ + # check whether the object has the request attribute + self.visit(node.expr) + self.emit('STORE_NAME', 'obj') + self.emit('LOAD_GLOBAL', 'hasattr') + self.emit('LOAD_NAME', 'obj') + self.emit('LOAD_CONST', node.attrname) + self.emit('CALL_FUNCTION', 2) + else_ = self.newBlock() + self.emit('JUMP_IF_FALSE', else_) + self.emit('POP_TOP') - def _visit_dict(self, node, data): - return dict([(self._visit(k, data), self._visit(v, data)) - for k, v in zip(node.keys, node.values)]) - - def _visit_list(self, node, data): - return [self._visit(n, data) for n in node.elts] + # hasattr returned True, so return the attribute value + self.emit('LOAD_NAME', 'obj') + self.emit('LOAD_ATTR', node.attrname) + self.emit('STORE_NAME', 'val') + return_ = self.newBlock() + self.emit('JUMP_FORWARD', return_) - def _visit_name(self, node, data): - val = data.get(node.id) - if val is None: - val = getattr(__builtin__, node.id, None) - return val + # hasattr returned False, so try item access + self.startBlock(else_) + try_ = self.newBlock() + except_ = self.newBlock() + self.emit('SETUP_EXCEPT', except_) + self.nextBlock(try_) + self.setups.push((pycodegen.EXCEPT, try_)) + self.emit('LOAD_NAME', 'obj') + self.emit('LOAD_CONST', node.attrname) + self.emit('BINARY_SUBSCR') + self.emit('STORE_NAME', 'val') + self.emit('POP_BLOCK') + self.setups.pop() + self.emit('JUMP_FORWARD', return_) - def _visit_num(self, node, data): - return node.n + # exception handler: just return `None` + self.startBlock(except_) + self.emit('DUP_TOP') + self.emit('LOAD_GLOBAL', 'KeyError') + self.emit('LOAD_GLOBAL', 'TypeError') + self.emit('BUILD_TUPLE', 2) + self.emit('COMPARE_OP', 'exception match') + next = self.newBlock() + self.emit('JUMP_IF_FALSE', next) + self.nextBlock() + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('LOAD_CONST', None) # exception handler body + self.emit('STORE_NAME', 'val') + self.emit('JUMP_FORWARD', return_) + self.nextBlock(next) + self.emit('POP_TOP') + self.emit('END_FINALLY') + + # return + self.nextBlock(return_) + self.emit('LOAD_NAME', 'val') - def _visit_str(self, node, data): - return node.s + def visitName(self, node): + """Overridden to lookup names in the context data instead of in + locals/globals. + + If a name is not found in the context data, we fall back to Python + builtins. + """ + next = self.newBlock() + end = self.newBlock() - def _visit_tuple(self, node, data): - return tuple([self._visit(n, data) for n in node.elts]) + # default: lookup in context data + self.loadName('data') + self.emit('LOAD_ATTR', 'get') + self.emit('LOAD_CONST', node.name) + self.emit('CALL_FUNCTION', 1) + self.emit('STORE_NAME', 'val') + + # test whether the value "is None" + self.emit('LOAD_NAME', 'val') + self.emit('LOAD_CONST', None) + self.emit('COMPARE_OP', 'is') + self.emit('JUMP_IF_FALSE', next) + self.emit('POP_TOP') + + # if it is, fallback to builtins + self.emit('LOAD_GLOBAL', 'getattr') + self.emit('LOAD_GLOBAL', '__builtin__') + self.emit('LOAD_CONST', node.name) + self.emit('LOAD_CONST', None) + self.emit('CALL_FUNCTION', 3) + self.emit('STORE_NAME', 'val') + self.emit('JUMP_FORWARD', end) + + self.nextBlock(next) + self.emit('POP_TOP') + + self.nextBlock(end) + self.emit('LOAD_NAME', 'val') + + def visitSubscript(self, node, aug_flag=None): + """Overridden to fallback to attribute access if the object doesn't + have an item (or doesn't even support item access). + + If either method fails, this returns `None` instead of raising an + `IndexError`, `KeyError`, or `TypeError`. + """ + self.visit(node.expr) + self.emit('STORE_NAME', 'obj') + + if len(node.subs) > 1: + # For non-scalar subscripts, use the default method + # FIXME: this should catch exceptions + self.emit('LOAD_NAME', 'obj') + for sub in node.subs: + self.visit(sub) + self.emit('BUILD_TUPLE', len(node.subs)) + self.emit('BINARY_SUBSCR') + + else: + # For a scalar subscript, fallback to attribute access + # FIXME: Would be nice if we could limit this to string subscripts + try_ = self.newBlock() + except_ = self.newBlock() + return_ = self.newBlock() + self.emit('SETUP_EXCEPT', except_) + self.nextBlock(try_) + self.setups.push((pycodegen.EXCEPT, try_)) + self.emit('LOAD_NAME', 'obj') + self.visit(node.subs[0]) + self.emit('BINARY_SUBSCR') + self.emit('STORE_NAME', 'val') + self.emit('POP_BLOCK') + self.setups.pop() + self.emit('JUMP_FORWARD', return_) + + self.startBlock(except_) + self.emit('DUP_TOP') + self.emit('LOAD_GLOBAL', 'KeyError') + self.emit('LOAD_GLOBAL', 'IndexError') + self.emit('LOAD_GLOBAL', 'TypeError') + self.emit('BUILD_TUPLE', 3) + self.emit('COMPARE_OP', 'exception match') + next = self.newBlock() + self.emit('JUMP_IF_FALSE', next) + self.nextBlock() + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('POP_TOP') + self.emit('LOAD_GLOBAL', 'getattr') # exception handler body + self.emit('LOAD_NAME', 'obj') + self.visit(node.subs[0]) + self.emit('LOAD_CONST', None) + self.emit('CALL_FUNCTION', 3) + self.emit('STORE_NAME', 'val') + self.emit('JUMP_FORWARD', return_) + self.nextBlock(next) + self.emit('POP_TOP') + self.emit('END_FINALLY') + + # return + self.nextBlock(return_) + self.emit('LOAD_NAME', 'val') + + +if __name__ == '__main__': + import doctest + doctest.testmod()
--- a/markup/template.py +++ b/markup/template.py @@ -32,9 +32,6 @@ Todo items: * Improved error reporting * Support for list comprehensions and generator expressions in expressions - -Random thoughts: - * Could we generate byte code from expressions? """ try: @@ -175,8 +172,12 @@ """ __slots__ = ['expr'] - def __init__(self, value): - self.expr = value and Expression(value) or None + def __init__(self, value, filename=None, lineno=-1, offset=-1): + try: + self.expr = value and Expression(value, filename, lineno) or None + except SyntaxError, err: + raise TemplateSyntaxError(err, filename, lineno, + offset + (err.offset or 0)) def __call__(self, stream, ctxt, directives): raise NotImplementedError @@ -319,8 +320,8 @@ ATTRIBUTE = 'function' - def __init__(self, args): - Directive.__init__(self, None) + def __init__(self, args, filename=None, lineno=-1, offset=-1): + Directive.__init__(self, None, filename, lineno, offset) ast = compiler.parse(args, 'eval').node self.args = [] self.defaults = {} @@ -374,10 +375,10 @@ ATTRIBUTE = 'each' - def __init__(self, value): + def __init__(self, value, filename=None, lineno=-1, offset=-1): targets, value = value.split(' in ', 1) self.targets = [str(name.strip()) for name in targets.split(',')] - Directive.__init__(self, value) + Directive.__init__(self, value, filename, lineno, offset) def __call__(self, stream, ctxt, directives): iterable = self.expr.evaluate(ctxt) @@ -442,8 +443,8 @@ ATTRIBUTE = 'path' - def __init__(self, value): - Directive.__init__(self, None) + def __init__(self, value, filename=None, lineno=-1, offset=-1): + Directive.__init__(self, None, filename, lineno, offset) self.path = Path(value) self.stream = [] @@ -716,7 +717,7 @@ if cls is None: raise BadDirectiveError(tag, pos[0], pos[1]) value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '') - directives.append(cls(value)) + directives.append(cls(value, *pos)) strip = True new_attrib = [] @@ -725,10 +726,12 @@ cls = self._dir_by_name.get(name.localname) if cls is None: raise BadDirectiveError(name, pos[0], pos[1]) - directives.append(cls(value)) + directives.append(cls(value, *pos)) else: if value: value = list(self._interpolate(value, *pos)) + if len(value) == 1 and value[0][0] is TEXT: + value = value[0][1] else: value = [(TEXT, u'', pos)] new_attrib.append((name, value)) @@ -781,7 +784,12 @@ def _interpolate(text, patterns): for idx, group in enumerate(patterns.pop(0).split(text)): if idx % 2: - yield EXPR, Expression(group), (lineno, offset) + try: + yield EXPR, Expression(group, filename, lineno), \ + (lineno, offset) + except SyntaxError, err: + raise TemplateSyntaxError(err, filename, lineno, + offset + (err.offset or 0)) elif group: if patterns: for result in _interpolate(group, patterns[:]): @@ -822,9 +830,11 @@ """Internal stream filter that evaluates any expressions in `START` and `TEXT` events. """ + filters = (self._ensure, self._eval, self._match) + for kind, data, pos in stream: - if kind is START: + if kind is START and data[1]: # Attributes may still contain expressions in start tags at # this point, so do some evaluation tag, attrib = data @@ -860,7 +870,7 @@ # case we yield the individual items try: substream = iter(result) - for filter_ in [self._ensure, self._eval, self._match]: + for filter_ in filters: substream = filter_(substream, ctxt) for event in substream: yield event @@ -874,23 +884,19 @@ def _flatten(self, stream, ctxt=None): """Internal stream filter that expands `SUB` events in the stream.""" - try: - for kind, data, pos in stream: - if kind is SUB: - # This event is a list of directives and a list of nested - # events to which those directives should be applied - directives, substream = data - substream = _apply_directives(substream, ctxt, directives) - for filter_ in (self._eval, self._match, self._flatten): - substream = filter_(substream, ctxt) - for event in substream: - yield event - continue - else: - yield kind, data, pos - except SyntaxError, err: - raise TemplateSyntaxError(err, pos[0], pos[1], - pos[2] + (err.offset or 0)) + for kind, data, pos in stream: + if kind is SUB: + # This event is a list of directives and a list of nested + # events to which those directives should be applied + directives, substream = data + substream = _apply_directives(substream, ctxt, directives) + for filter_ in (self._eval, self._match, self._flatten): + substream = filter_(substream, ctxt) + for event in substream: + yield event + continue + else: + yield kind, data, pos def _match(self, stream, ctxt=None, match_templates=None): """Internal stream filter that applies any defined match templates
--- a/markup/tests/eval.py +++ b/markup/tests/eval.py @@ -170,9 +170,14 @@ def test_compare_multi(self): self.assertEqual(True, Expression("1 != 3 == 3").evaluate({})) - self.assertEqual(True, Expression("x != y == y").evaluate({'x': 3, + self.assertEqual(True, Expression("x != y == y").evaluate({'x': 1, 'y': 3})) + # FIXME: need support for local names in comprehensions + #def test_list_comprehension(self): + # expr = Expression("[n for n in numbers if n < 2]") + # self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) + def suite(): suite = unittest.TestSuite()
--- a/markup/tests/template.py +++ b/markup/tests/template.py @@ -473,10 +473,9 @@ self.assertEqual(1, e.lineno) def test_directive_value_syntax_error(self): - xml = '<p xmlns:py="http://markup.edgewall.org/" py:if="bar\'" />' - tmpl = Template(xml, filename='test.html') + xml = """<p xmlns:py="http://markup.edgewall.org/" py:if="bar'" />""" try: - list(tmpl.generate()) + tmpl = Template(xml, filename='test.html') self.fail('Expected SyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) @@ -485,17 +484,16 @@ # We don't really care about the offset here, do we? def test_expression_syntax_error(self): - xml = '<p>\n Foo <em>${bar"}</em>\n</p>' - tmpl = Template(xml, filename='test.html') - ctxt = Context(bar='baz') + xml = """<p> + Foo <em>${bar"}</em> + </p>""" try: - list(tmpl.generate(ctxt)) + tmpl = Template(xml, filename='test.html') self.fail('Expected SyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) if sys.version_info[:2] >= (2, 4): self.assertEqual(2, e.lineno) - self.assertEqual(10, e.offset) def test_markup_noescape(self): """