root/trunk/airspeed.py

Revision 57, 25.0 kB (checked in by steve, 2 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
3 import re, operator, os
4
5 import StringIO   # cStringIO has issues with unicode
6
7 __all__ = ['Template', 'TemplateError', 'TemplateSyntaxError', 'CachingFileLoader']
8
9
10 ###############################################################################
11 # Compatibility for old Pythons & Jython
12 ###############################################################################
13 try: True
14 except NameError:
15     False, True = 0, 1
16 try: dict
17 except NameError:
18     from UserDict import UserDict
19     class dict(UserDict):
20         def __init__(self): self.data = {}
21 try: operator.__gt__
22 except 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
30 try:
31     basestring
32     def is_string(s): return isinstance(s, basestring)
33 except NameError:
34     def is_string(s): return type(s) == type('')
35
36 ###############################################################################
37 # Public interface
38 ###############################################################################
39
40 def boolean_value(variable_value):
41     if variable_value == False: return False
42     return not (variable_value is None)
43
44
45 class 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
65 class TemplateError(Exception):
66     pass
67
68
69 class 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
94 class 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
102 class 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
131 class NoMatch(Exception): pass
132
133
134 class 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
155 class _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
229 class 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
243 class 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
254 class 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
267 class 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
281 class 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
293 class 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
311 class _EmptyValues:
312     def calculate(self, namespace, loader):
313         return []
314
315
316 class 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
331 class 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
339 class 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
368 class 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
379 class 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
396 class 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
412 class 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
432 class 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
441 class Null:
442     def evaluate(self, stream, namespace, loader): pass
443
444
445 class 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
452 class 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
465 class 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
477 class 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
500 class 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
507 class 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
516 class 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
527 class 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
554 class 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
567 class 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
605 class 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
630 class 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
645 class 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
662 class 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
673 class 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
710 class 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
721 class 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.