root/trunk/airspeed.py

Revision 57, 25.0 KB (checked in by steve, 5 years ago)

Fix for #16 (superfluous explicit self argument in call to self.syntax_error) (thanks Zoran Isailovski)

  • Property svn:eol-style set to native
Line 
1#!/usr/bin/env python
2
3import re, operator, os
4
5import StringIO   # cStringIO has issues with unicode
6
7__all__ = ['Template', 'TemplateError', 'TemplateSyntaxError', 'CachingFileLoader']
8
9
10###############################################################################
11# Compatibility for old Pythons & Jython
12###############################################################################
13try: True
14except NameError:
15    False, True = 0, 1
16try: dict
17except NameError:
18    from UserDict import UserDict
19    class dict(UserDict):
20        def __init__(self): self.data = {}
21try: operator.__gt__
22except AttributeError:
23    operator.__gt__ = lambda a, b: a > b
24    operator.__lt__ = lambda a, b: a < b
25    operator.__ge__ = lambda a, b: a >= b
26    operator.__le__ = lambda a, b: a <= b
27    operator.__eq__ = lambda a, b: a == b
28    operator.__ne__ = lambda a, b: a != b
29    operator.mod = lambda a, b: a % b
30try:
31    basestring
32    def is_string(s): return isinstance(s, basestring)
33except NameError:
34    def is_string(s): return type(s) == type('')
35
36###############################################################################
37# Public interface
38###############################################################################
39
40def boolean_value(variable_value):
41    if variable_value == False: return False
42    return not (variable_value is None)
43
44
45class Template:
46    def __init__(self, content):
47        self.content = content
48        self.root_element = None
49
50    def merge(self, namespace, loader=None):
51        output = StringIO.StringIO()
52        self.merge_to(namespace, output, loader)
53        return output.getvalue()
54
55    def ensure_compiled(self):
56        if not self.root_element:
57            self.root_element = TemplateBody(self.content)
58
59    def merge_to(self, namespace, fileobj, loader=None):
60        if loader is None: loader = NullLoader()
61        self.ensure_compiled()
62        self.root_element.evaluate(fileobj, namespace, loader)
63
64
65class TemplateError(Exception):
66    pass
67
68
69class TemplateSyntaxError(TemplateError):
70    def __init__(self, element, expected):
71        self.element = element
72        self.text_understood = element.full_text()[:element.end]
73        self.line = 1 + self.text_understood.count('\n')
74        self.column = len(self.text_understood) - self.text_understood.rfind('\n')
75        got = element.next_text()
76        if len(got) > 40:
77            got = got[:36] + ' ...'
78        Exception.__init__(self, "line %d, column %d: expected %s in %s, got: %s ..." % (self.line, self.column, expected, self.element_name(), got))
79
80    def get_position_strings(self):
81        error_line_start = 1 + self.text_understood.rfind('\n')
82        if '\n' in self.element.next_text():
83            error_line_end = self.element.next_text().find('\n') + self.element.end
84        else:
85            error_line_end = len(self.element.full_text())
86        error_line = self.element.full_text()[error_line_start:error_line_end]
87        caret_pos = self.column
88        return [error_line, ' ' * (caret_pos - 1) + '^']
89
90    def element_name(self):
91        return re.sub('([A-Z])', lambda m: ' ' + m.group(1).lower(), self.element.__class__.__name__).strip()
92
93
94class NullLoader:
95    def load_text(self, name):
96        raise TemplateError("no loader available for '%s'" % name)
97
98    def load_template(self, name):
99        raise self.load_text(name)
100
101
102class CachingFileLoader:
103    def __init__(self, basedir):
104        self.basedir = basedir
105        self.known_templates = {} # name -> (template, file_mod_time)
106
107    def filename_of(self, name):
108        return os.path.join(self.basedir, name)
109
110    def load_text(self, name):
111        f = open(os.path.join(self.basedir, name))
112        try: return f.read()
113        finally: f.close()
114
115    def load_template(self, name):
116        mtime = os.path.getmtime(self.filename_of(name))
117        if self.known_templates.has_key(name):
118            template, prev_mtime = self.known_templates[name]
119            if mtime <= prev_mtime:
120                return template
121        template = Template(self.load_text(name))
122        template.ensure_compiled()
123        self.known_templates[name] = (template, mtime)
124        return template
125
126
127###############################################################################
128# Internals
129###############################################################################
130
131class NoMatch(Exception): pass
132
133
134class LocalNamespace(dict):
135    def __init__(self, parent):
136        dict.__init__(self)
137        self.parent = parent
138
139    def __getitem__(self, key):
140        try: return dict.__getitem__(self, key)
141        except KeyError:
142            parent_value = self.parent[key]
143            self[key] = parent_value
144            return parent_value
145
146    def top(self):
147        if hasattr(self.parent, "top"):
148            return self.parent.top()
149        return self.parent
150
151    def __repr__(self):
152        return dict.__repr__(self) + '->' + repr(self.parent)
153
154
155class _Element:
156    def __init__(self, text, start=0):
157        self._full_text = text
158        self.start = self.end = start
159        self.parse()
160
161    def next_text(self):
162        return self._full_text[self.end:]
163
164    def my_text(self):
165        return self._full_text[self.start:self.end]
166
167    def full_text(self):
168        return self._full_text
169
170    def syntax_error(self, expected):
171        return TemplateSyntaxError(self, expected)
172
173    def identity_match(self, pattern):
174        m = pattern.match(self._full_text, self.end)
175        if not m: raise NoMatch()
176        self.end = m.start(pattern.groups)
177        return m.groups()[:-1]
178
179    def next_match(self, pattern):
180        m = pattern.match(self._full_text, self.end)
181        if not m: return False
182        self.end = m.start(pattern.groups)
183        return m.groups()[:-1]
184
185    def optional_match(self, pattern):
186        m = pattern.match(self._full_text, self.end)
187        if not m: return False
188        self.end = m.start(pattern.groups)
189        return True
190
191    def require_match(self, pattern, expected):
192        m = pattern.match(self._full_text, self.end)
193        if not m: raise self.syntax_error(expected)
194        self.end = m.start(pattern.groups)
195        return m.groups()[:-1]
196
197    def next_element(self, element_spec):
198        if callable(element_spec):
199            element = element_spec(self._full_text, self.end)
200            self.end = element.end
201            return element
202        else:
203            for element_class in element_spec:
204                try: element = element_class(self._full_text, self.end)
205                except NoMatch: pass
206                else:
207                    self.end = element.end
208                    return element
209            raise NoMatch()
210
211    def require_next_element(self, element_spec, expected):
212        if callable(element_spec):
213            try: element = element_spec(self._full_text, self.end)
214            except NoMatch: raise self.syntax_error(expected)
215            else:
216                self.end = element.end
217                return element
218        else:
219            for element_class in element_spec:
220                try: element = element_class(self._full_text, self.end)
221                except NoMatch: pass
222                else:
223                    self.end = element.end
224                    return element
225            expected = ', '.join([cls.__name__ for cls in element_spec])
226            raise self.syntax_error('one of: ' + expected)
227
228
229class Text(_Element):
230    PLAIN = re.compile(r'((?:[^\\\$#]+|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|\\.)(.*)$', re.S + re.I)
231    ESCAPED_CHAR = re.compile(r'\\([\\\$#])')
232
233    def parse(self):
234        text, = self.identity_match(self.PLAIN)
235        def unescape(match):
236            return match.group(1)
237        self.text = self.ESCAPED_CHAR.sub(unescape, text)
238
239    def evaluate(self, stream, namespace, loader):
240        stream.write(self.text)
241
242
243class IntegerLiteral(_Element):
244    INTEGER = re.compile(r'(\d+)(.*)', re.S)
245
246    def parse(self):
247        self.value, = self.identity_match(self.INTEGER)
248        self.value = int(self.value)
249
250    def calculate(self, namespace, loader):
251        return self.value
252
253
254class StringLiteral(_Element):
255    STRING = re.compile(r"'((?:\\['nrbt\\\\\\$]|[^'\n\r\\])*)'(.*)", re.S)
256    ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])")
257
258    def parse(self):
259        value, = self.identity_match(self.STRING)
260        def unescape(match):
261            return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\', "'": "'"}.get(match.group(1), '\\' + match.group(1))
262        self.value = self.ESCAPED_CHAR.sub(unescape, value)
263
264    def calculate(self, namespace, loader):
265        return self.value
266
267class InterpolatedStringLiteral(StringLiteral):
268    STRING = re.compile(r'"((?:\\["nrbt\\\\\\$]|[^"\n\r\\])*)"(.*)', re.S)
269    ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])')
270
271    def parse(self):
272        StringLiteral.parse(self)
273        self.block = Block(self.value, 0)
274
275    def calculate(self, namespace, loader):
276        output = StringIO.StringIO()
277        self.block.evaluate(output, namespace, loader)
278        return output.getvalue()
279
280
281class Range(_Element):
282    RANGE = re.compile(r'(\-?\d+)[ \t]*\.\.[ \t]*(\-?\d+)(.*)$', re.S)
283
284    def parse(self):
285        self.value1, self.value2 = map(int, self.identity_match(self.RANGE))
286
287    def calculate(self, namespace, loader):
288        if self.value2 < self.value1:
289            return xrange(self.value1, self.value2 - 1, -1)
290        return xrange(self.value1, self.value2 + 1)
291
292
293class ValueList(_Element):
294    COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
295
296    def parse(self):
297        self.values = []
298        try: value = self.next_element(Value)
299        except NoMatch:
300            pass
301        else:
302            self.values.append(value)
303            while self.optional_match(self.COMMA):
304                value = self.require_next_element(Value, 'value')
305                self.values.append(value)
306
307    def calculate(self, namespace, loader):
308        return [value.calculate(namespace, loader) for value in self.values]
309
310
311class _EmptyValues:
312    def calculate(self, namespace, loader):
313        return []
314
315
316class ArrayLiteral(_Element):
317    START = re.compile(r'\[[ \t]*(.*)$', re.S)
318    END =   re.compile(r'[ \t]*\](.*)$', re.S)
319    values = _EmptyValues()
320
321    def parse(self):
322        self.identity_match(self.START)
323        try:
324            self.values = self.next_element((Range, ValueList))
325        except NoMatch:
326            pass
327        self.require_match(self.END, ']')
328        self.calculate = self.values.calculate
329
330
331class Value(_Element):
332    def parse(self):
333        self.expression = self.next_element((SimpleReference, IntegerLiteral, StringLiteral, InterpolatedStringLiteral, ArrayLiteral, Condition, UnaryOperatorValue))
334
335    def calculate(self, namespace, loader):
336        return self.expression.calculate(namespace, loader)
337
338
339class NameOrCall(_Element):
340    NAME = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S)
341    parameters = None
342
343    def parse(self):
344        self.name, = self.identity_match(self.NAME)
345        try: self.parameters = self.next_element(ParameterList)
346        except NoMatch: pass
347
348    def calculate(self, current_object, loader, top_namespace):
349        look_in_dict = True
350        if not isinstance(current_object, LocalNamespace):
351            try:
352                result = getattr(current_object, self.name)
353                look_in_dict = False
354            except AttributeError:
355                pass
356        if look_in_dict:
357            try: result = current_object[self.name]
358            except KeyError: result = None
359            except TypeError: result = None
360            except AttributeError: result = None
361        if result is None:
362            return None ## TODO: an explicit 'not found' exception?
363        if self.parameters is not None:
364            result = result(*self.parameters.calculate(top_namespace, loader))
365        return result
366
367
368class SubExpression(_Element):
369    DOT = re.compile('\.(.*)', re.S)
370
371    def parse(self):
372        self.identity_match(self.DOT)
373        self.expression = self.next_element(VariableExpression)
374
375    def calculate(self, current_object, loader, global_namespace):
376        return self.expression.calculate(current_object, loader, global_namespace)
377
378
379class VariableExpression(_Element):
380    subexpression = None
381
382    def parse(self):
383        self.part = self.next_element(NameOrCall)
384        try: self.subexpression = self.next_element(SubExpression)
385        except NoMatch: pass
386
387    def calculate(self, namespace, loader, global_namespace=None):
388        if global_namespace is None:
389            global_namespace = namespace
390        value = self.part.calculate(namespace, loader, global_namespace)
391        if self.subexpression:
392            value = self.subexpression.calculate(value, loader, global_namespace)
393        return value
394
395
396class ParameterList(_Element):
397    START = re.compile(r'\(\s*(.*)$', re.S)
398    COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
399    END = re.compile(r'\s*\)(.*)$', re.S)
400    values = _EmptyValues()
401
402    def parse(self):
403        self.identity_match(self.START)
404        try: self.values = self.next_element(ValueList)
405        except NoMatch: pass
406        self.require_match(self.END, ')')
407
408    def calculate(self, namespace, loader):
409        return self.values.calculate(namespace, loader)
410
411
412class Placeholder(_Element):
413    START = re.compile(r'\$(!?)(\{?)(.*)$', re.S)
414    CLOSING_BRACE = re.compile(r'\}(.*)$', re.S)
415
416    def parse(self):
417        self.silent, self.braces = self.identity_match(self.START)
418        self.expression = self.require_next_element(VariableExpression, 'expression')
419        if self.braces: self.require_match(self.CLOSING_BRACE, '}')
420
421    def evaluate(self, stream, namespace, loader):
422        value = self.expression.calculate(namespace, loader)
423        if value is None:
424            if self.silent: value = ''
425            else: value = self.my_text()
426        if is_string(value):
427            stream.write(value)
428        else:
429            stream.write(str(value))
430
431
432class SimpleReference(_Element):
433    LEADING_DOLLAR = re.compile('\$(.*)', re.S)
434
435    def parse(self):
436        self.identity_match(self.LEADING_DOLLAR)
437        self.expression = self.require_next_element(VariableExpression, 'name')
438        self.calculate = self.expression.calculate
439
440
441class Null:
442    def evaluate(self, stream, namespace, loader): pass
443
444
445class Comment(_Element, Null):
446    COMMENT = re.compile('#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S)
447
448    def parse(self):
449        self.identity_match(self.COMMENT)
450
451
452class BinaryOperator(_Element):
453    BINARY_OP = re.compile(r'\s*(>=|<=|<|==|!=|>|%|\|\||&&)\s*(.*)$', re.S)
454    OPERATORS = {'>' : operator.__gt__, '>=': operator.__ge__,
455                 '<' : operator.__lt__, '<=': operator.__le__,
456                 '==': operator.__eq__, '!=': operator.__ne__,
457                 '%' : operator.mod,
458                 '||': lambda a,b : boolean_value(a) or boolean_value(b),
459                 '&&': lambda a,b : boolean_value(a) and boolean_value(b)}
460    def parse(self):
461        op_string, = self.identity_match(self.BINARY_OP)
462        self.apply_to = self.OPERATORS[op_string]
463
464
465class UnaryOperatorValue(_Element):
466    UNARY_OP = re.compile(r'\s*(!)\s*(.*)$', re.S)
467    OPERATORS = {'!': operator.__not__}
468    def parse(self):
469        op_string, = self.identity_match(self.UNARY_OP)
470        self.value = self.next_element(Value)
471        self.op = self.OPERATORS[op_string]
472
473    def calculate(self, namespace, loader):
474        return self.op(self.value.calculate(namespace, loader))
475
476
477class Condition(_Element):
478    START = re.compile(r'\(\s*(.*)$', re.S)
479    END = re.compile(r'\s*\)(.*)$', re.S)
480    binary_operator = None
481    value2 = None
482
483    def parse(self):
484        self.identity_match(self.START)
485        self.value = self.next_element(Value)
486        try:
487            self.binary_operator = self.next_element(BinaryOperator)
488            self.value2 = self.require_next_element(Value, 'value')
489        except NoMatch:
490            pass
491        self.require_match(self.END, ') or >')
492
493    def calculate(self, namespace, loader):
494        if self.binary_operator is None:
495            return self.value.calculate(namespace, loader)
496        value1, value2 = self.value.calculate(namespace, loader), self.value2.calculate(namespace, loader)
497        return self.binary_operator.apply_to(value1, value2)
498
499
500class End(_Element):
501    END = re.compile(r'#end(.*)', re.I + re.S)
502
503    def parse(self):
504        self.identity_match(self.END)
505
506
507class ElseBlock(_Element):
508    START = re.compile(r'#else(.*)$', re.S + re.I)
509
510    def parse(self):
511        self.identity_match(self.START)
512        self.block = self.require_next_element(Block, 'block')
513        self.evaluate = self.block.evaluate
514
515
516class ElseifBlock(_Element):
517    START = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I)
518
519    def parse(self):
520        self.identity_match(self.START)
521        self.condition = self.require_next_element(Condition, 'condition')
522        self.block = self.require_next_element(Block, 'block')
523        self.calculate = self.condition.calculate
524        self.evaluate = self.block.evaluate
525
526
527class IfDirective(_Element):
528    START = re.compile(r'#if\b\s*(.*)$', re.S + re.I)
529    else_block = Null()
530
531    def parse(self):
532        self.identity_match(self.START)
533        self.condition = self.next_element(Condition)
534        self.block = self.require_next_element(Block, "block")
535        self.elseifs = []
536        while True:
537            try: self.elseifs.append(self.next_element(ElseifBlock))
538            except NoMatch: break
539        try: self.else_block = self.next_element(ElseBlock)
540        except NoMatch: pass
541        self.require_next_element(End, '#else, #elseif or #end')
542
543    def evaluate(self, stream, namespace, loader):
544        if self.condition.calculate(namespace, loader):
545            self.block.evaluate(stream, namespace, loader)
546        else:
547            for elseif in self.elseifs:
548                if elseif.calculate(namespace, loader):
549                    elseif.evaluate(stream, namespace, loader)
550                    return
551            self.else_block.evaluate(stream, namespace, loader)
552
553
554class Assignment(_Element):
555    START = re.compile(r'\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S + re.I)
556    END = re.compile(r'\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M)
557
558    def parse(self):
559        self.var_name, = self.identity_match(self.START)
560        self.value = self.require_next_element(Value, "value")
561        self.require_match(self.END, ')')
562
563    def evaluate(self, stream, namespace, loader):
564        namespace[self.var_name] = self.value.calculate(namespace, loader)
565
566
567class MacroDefinition(_Element):
568    START = re.compile(r'#macro\b(.*)', re.S + re.I)
569    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
570    NAME = re.compile(r'\s*([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
571    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
572    ARG_NAME = re.compile(r'[ \t]+\$([a-z][a-z_0-9]*)(.*)$', re.S + re.I)
573    RESERVED_NAMES = ('if', 'else', 'elseif', 'set', 'macro', 'foreach', 'parse', 'include', 'stop', 'end')
574    def parse(self):
575        self.identity_match(self.START)
576        self.require_match(self.OPEN_PAREN, '(')
577        self.macro_name, = self.require_match(self.NAME, 'macro name')
578        if self.macro_name.lower() in self.RESERVED_NAMES:
579            raise self.syntax_error('non-reserved name')
580        self.arg_names = []
581        while True:
582            m = self.next_match(self.ARG_NAME)
583            if not m: break
584            self.arg_names.append(m[0])
585        self.require_match(self.CLOSE_PAREN, ') or arg name')
586        self.block = self.require_next_element(Block, 'block')
587        self.require_next_element(End, 'block')
588
589    def evaluate(self, stream, namespace, loader):
590        global_ns = namespace.top()
591        macro_key = '#' + self.macro_name.lower()
592        if global_ns.has_key(macro_key):
593            raise Exception("cannot redefine macro")
594        global_ns[macro_key] = self
595
596    def execute_macro(self, stream, namespace, arg_value_elements, loader):
597        if len(arg_value_elements) != len(self.arg_names):
598            raise Exception("expected %d arguments, got %d" % (len(self.arg_names), len(arg_value_elements)))
599        macro_namespace = LocalNamespace(namespace)
600        for arg_name, arg_value in zip(self.arg_names, arg_value_elements):
601            macro_namespace[arg_name] = arg_value.calculate(namespace, loader)
602        self.block.evaluate(stream, macro_namespace, loader)
603
604
605class MacroCall(_Element):
606    START = re.compile(r'#([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
607    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
608    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
609    SPACE = re.compile(r'[ \t]+(.*)$', re.S)
610
611    def parse(self):
612        self.macro_name, = self.identity_match(self.START)
613        self.macro_name = self.macro_name.lower()
614        self.args = []
615        if self.macro_name in MacroDefinition.RESERVED_NAMES or self.macro_name.startswith('end'):
616            raise NoMatch()
617        self.require_match(self.OPEN_PAREN, '(')
618        while True:
619            try: self.args.append(self.next_element(Value))
620            except NoMatch: break
621            if not self.optional_match(self.SPACE): break
622        self.require_match(self.CLOSE_PAREN, 'argument value or )')
623
624    def evaluate(self, stream, namespace, loader):
625        try: macro = namespace['#' + self.macro_name]
626        except KeyError: raise Exception('no such macro: ' + self.macro_name)
627        macro.execute_macro(stream, namespace, self.args, loader)
628
629
630class IncludeDirective(_Element):
631    START = re.compile(r'#include\b(.*)', re.S + re.I)
632    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
633    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
634
635    def parse(self):
636        self.identity_match(self.START)
637        self.require_match(self.OPEN_PAREN, '(')
638        self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, SimpleReference), 'template name')
639        self.require_match(self.CLOSE_PAREN, ')')
640
641    def evaluate(self, stream, namespace, loader):
642        stream.write(loader.load_text(self.name.calculate(namespace, loader)))
643
644
645class ParseDirective(_Element):
646    START = re.compile(r'#parse\b(.*)', re.S + re.I)
647    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
648    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
649
650    def parse(self):
651        self.identity_match(self.START)
652        self.require_match(self.OPEN_PAREN, '(')
653        self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, SimpleReference), 'template name')
654        self.require_match(self.CLOSE_PAREN, ')')
655
656    def evaluate(self, stream, namespace, loader):
657        template = loader.load_template(self.name.calculate(namespace, loader))
658        ## TODO: local namespace?
659        template.merge_to(namespace, stream, loader=loader)
660
661
662class SetDirective(_Element):
663    START = re.compile(r'#set\b(.*)', re.S + re.I)
664
665    def parse(self):
666        self.identity_match(self.START)
667        self.assignment = self.require_next_element(Assignment, 'assignment')
668
669    def evaluate(self, stream, namespace, loader):
670        self.assignment.evaluate(stream, namespace, loader)
671
672
673class ForeachDirective(_Element):
674    START = re.compile(r'#foreach\b(.*)$', re.S + re.I)
675    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
676    IN = re.compile(r'[ \t]+in[ \t]+(.*)$', re.S)
677    LOOP_VAR_NAME = re.compile(r'\$([a-z_][a-z0-9_]*)(.*)$', re.S + re.I)
678    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
679
680    def parse(self):
681        ## Could be cleaner b/c syntax error if no '('
682        self.identity_match(self.START)
683        self.require_match(self.OPEN_PAREN, '(')
684        self.loop_var_name, = self.require_match(self.LOOP_VAR_NAME, 'loop var name')
685        self.require_match(self.IN, 'in')
686        self.value = self.next_element(Value)
687        self.require_match(self.CLOSE_PAREN, ')')
688        self.block = self.next_element(Block)
689        self.require_next_element(End, '#end')
690
691    def evaluate(self, stream, namespace, loader):
692        iterable = self.value.calculate(namespace, loader)
693        counter = 1
694        try:
695            if iterable is None:
696                return
697            if hasattr(iterable, 'keys'): iterable = iterable.keys()
698            if not hasattr(iterable, '__getitem__'):
699                raise ValueError("value for $%s is not iterable in #foreach: %s" % (self.loop_var_name, iterable))
700            for item in iterable:
701                namespace = LocalNamespace(namespace)
702                namespace['velocityCount'] = counter
703                namespace[self.loop_var_name] = item
704                self.block.evaluate(stream, namespace, loader)
705                counter += 1
706        except TypeError:
707            raise
708
709
710class TemplateBody(_Element):
711    def parse(self):
712        self.block = self.next_element(Block)
713        if self.next_text():
714            raise self.syntax_error('block element')
715
716    def evaluate(self, stream, namespace, loader):
717        namespace = LocalNamespace(namespace)
718        self.block.evaluate(stream, namespace, loader)
719
720
721class Block(_Element):
722    def parse(self):
723        self.children = []
724        while True:
725            try: self.children.append(self.next_element((Text, Placeholder, Comment, IfDirective, SetDirective, ForeachDirective, IncludeDirective, ParseDirective, MacroDefinition, MacroCall)))
726            except NoMatch: break
727
728    def evaluate(self, stream, namespace, loader):
729        for child in self.children:
730            child.evaluate(stream, namespace, loader)
Note: See TracBrowser for help on using the browser.