Changeset 17
- Timestamp:
- 17/08/04 20:21:00 (4 years ago)
- Files:
-
- trunk/airspeed.py (modified) (18 diffs)
- trunk/airspeed_test.py (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/airspeed.py
r16 r17 3 3 import re 4 4 import cStringIO as StringIO 5 6 __all__ = ['TemplateSyntaxError', 'Template'] 7 8 ############################################################################### 9 # Public interface 10 ############################################################################### 11 12 class Template: 13 def __init__(self, content): 14 self.content = content 15 self.evaluator = None 16 17 def merge(self, namespace): 18 output = StringIO.StringIO() 19 self.merge_to(namespace, output) 20 return output.getvalue() 21 22 def merge_to(self, namespace, fileobj): 23 output = [] 24 if not self.evaluator: 25 self.evaluator = TemplateBody(self.content) 26 self.evaluator.evaluate(namespace, fileobj) 5 27 6 28 … … 12 34 13 35 36 ############################################################################### 37 # Internals 38 ############################################################################### 39 14 40 class NoMatch(Exception): pass 15 41 … … 19 45 dict.__init__(self) 20 46 self.parent = parent 47 21 48 def __getitem__(self, key): 22 49 try: return dict.__getitem__(self, key) … … 24 51 25 52 26 class TextElement: 53 class _Element: 54 def match_or_reject(self, pattern, text): 55 m = pattern.match(text) 56 if not m: raise NoMatch() 57 return m.groups() 58 59 def require_match(self, pattern, text, expected): 60 m = pattern.match(text) 61 if not m: raise TemplateSyntaxError(self, expected, text) 62 return m.groups() 63 64 def next_element(self, element_class, text): 65 element = element_class(text) 66 return element, element.remaining_text 67 68 def require_next_element(self, element_class, text, expected): 69 try: element = element_class(text) 70 except NoMatch: raise TemplateSyntaxError(self, expected, text) 71 else: return element, element.remaining_text 72 73 74 class Text(_Element): 27 75 MY_PATTERN = re.compile(r'^((?:[^\\\$#]|\\[\$#])+|\$[^!\{a-z0-9_]|\$$)(.*)$', re.S + re.I) 28 76 def __init__(self, text): 29 m = self.MY_PATTERN.match(text) 30 if not m: raise NoMatch() 31 self.text, self.remaining_text = m.groups() 77 self.text, self.remaining_text = self.match_or_reject(self.MY_PATTERN, text) 32 78 33 79 def evaluate(self, namespace, stream): … … 35 81 36 82 37 class IntegerLiteral Element:83 class IntegerLiteral(_Element): 38 84 MY_PATTERN = re.compile(r'^(\d+)(.*)', re.S) 39 85 def __init__(self, text): 40 m = self.MY_PATTERN.match(text) 41 if not m: raise NoMatch() 42 self.value = int(m.group(1)) 43 self.remaining_text = m.group(2) 86 self.value, self.remaining_text = self.match_or_reject(self.MY_PATTERN, text) 87 self.value = int(self.value) 44 88 45 89 def calculate(self, namespace): … … 47 91 48 92 49 class StringLiteral Element:93 class StringLiteral(_Element): 50 94 MY_PATTERN = re.compile(r'^"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S) 51 95 ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])') 52 96 def __init__(self, text): 53 m = self.MY_PATTERN.match(text) 54 if not m: raise NoMatch() 97 value, self.remaining_text = self.match_or_reject(self.MY_PATTERN, text) 55 98 def unescape(match): 56 99 return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\'}[match.group(1)] 57 self.value = self.ESCAPED_CHAR.sub(unescape, m.group(1)) 58 self.remaining_text = m.group(2) 100 self.value = self.ESCAPED_CHAR.sub(unescape, value) 59 101 60 102 def calculate(self, namespace): … … 62 104 63 105 64 class Value Element:106 class Value(_Element): 65 107 def __init__(self, text): 66 108 if text.startswith('$'): 67 self.expression = Expression Element(text[1:])109 self.expression = Expression(text[1:]) 68 110 else: 69 111 try: 70 self.expression = IntegerLiteral Element(text)112 self.expression = IntegerLiteral(text) 71 113 except NoMatch: 72 self.expression = StringLiteral Element(text)114 self.expression = StringLiteral(text) 73 115 self.remaining_text = self.expression.remaining_text 74 116 … … 77 119 78 120 79 class Expression Element:121 class Expression(_Element): 80 122 NAME_PATTERN = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S) 81 123 def __init__(self, text): … … 91 133 92 134 def read_name_or_call(self, text): 93 m = self.NAME_PATTERN.match(text) 94 if not m: raise NoMatch() 95 name, text = m.groups() 135 name, text = self.match_or_reject(self.NAME_PATTERN, text) 96 136 parameter_list = None 97 137 try: 98 parameter_list = ParameterListElement(text) 99 text = parameter_list.remaining_text 138 parameter_list, text = self.next_element(ParameterList, text) 100 139 except NoMatch: 101 140 pass … … 117 156 118 157 119 class ParameterList Element:158 class ParameterList(_Element): 120 159 OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 121 160 CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) … … 124 163 def __init__(self, text): 125 164 self.values = [] 126 m = self.OPENING_PATTERN.match(text) 127 if not m: raise NoMatch() 128 text = m.group(1) 129 while True: ## FIXME 130 m = self.COMMA_PATTERN.match(text) 131 if not m: break 132 value = ValueElement(m.group(1)) 133 text = value.remaining_text 165 text, = self.match_or_reject(self.OPENING_PATTERN, text) 166 try: value, text = self.next_element(Value, text) 167 except NoMatch: 168 pass 169 else: 134 170 self.values.append(value) 135 m = self.CLOSING_PATTERN.match(text) 136 if not m: raise TemplateSyntaxError(self, ')', text) 137 self.remaining_text = m.group(1) 138 139 140 class PlaceholderElement: 171 while True: 172 m = self.COMMA_PATTERN.match(text) 173 if not m: break 174 value, text = self.require_next_element(Value, m.group(1), 'value') 175 self.values.append(value) 176 self.remaining_text, = self.require_match(self.CLOSING_PATTERN, text, ')') 177 178 179 class Placeholder(_Element): 141 180 MY_PATTERN = re.compile(r'^\$(!?)(\{?)(.*)$', re.S) 142 181 CLOSING_BRACE_PATTERN = re.compile(r'^\}(.*)$', re.S) 143 182 def __init__(self, text): 144 m = self.MY_PATTERN.match(text) 145 if not m: raise NoMatch() 146 self.silent = bool(m.group(1)) 147 self.braces = bool(m.group(2)) 148 text = m.group(3) 149 try: 150 self.expression = ExpressionElement(text) 151 text = self.expression.remaining_text 152 except NoMatch: 153 raise TemplateSyntaxError(self, 'expression', text) 183 self.silent, self.braces, text = self.match_or_reject(self.MY_PATTERN, text) 184 self.expression, text = self.require_next_element(Expression, text, 'expression') 154 185 if self.braces: 155 m = self.CLOSING_BRACE_PATTERN.match(text) 156 if not m: 157 raise TemplateSyntaxError(self, '}', text) 158 text = m.group(1) 186 text, = self.require_match(self.CLOSING_BRACE_PATTERN, text, '}') 159 187 self.remaining_text = text 160 188 … … 170 198 171 199 172 class CommentElement: 173 COMMENT_PATTERN = re.compile('^##.*?(?:\n|$)(.*)$', re.M + re.S) 174 MULTI_LINE_COMMENT_PATTERN = re.compile('^#\*.*?\*#(?:[ \t]*\n)?(.*$)', re.M + re.S) 175 def __init__(self, text): 176 for pattern in (self.COMMENT_PATTERN, self.MULTI_LINE_COMMENT_PATTERN): 177 m = pattern.match(text) 178 if not m: continue 179 self.remaining_text = m.group(1) 180 return 181 raise NoMatch() 200 class Comment(_Element): 201 COMMENT_PATTERN = re.compile('^#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S) 202 def __init__(self, text): 203 self.remaining_text, = self.match_or_reject(self.COMMENT_PATTERN, text) 182 204 183 205 def evaluate(self, namespace, stream): … … 185 207 186 208 187 class Condition Element:209 class Condition(_Element): 188 210 OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 189 211 CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 190 212 def __init__(self, text): 191 m = self.OPENING_PATTERN.match(text) 192 if not m: raise TemplateSyntaxError(self, '(', text) 193 text = m.group(1) 194 self.expression = ValueElement(text) 195 text = self.expression.remaining_text 196 m = self.CLOSING_PATTERN.match(text) 197 if not m: raise TemplateSyntaxError(self, ')', text) 198 self.remaining_text = m.group(1) 213 text, = self.require_match(self.OPENING_PATTERN, text, '(') 214 self.expression, text = self.next_element(Value, text) 215 self.remaining_text, = self.require_match(self.CLOSING_PATTERN, text, ')') 199 216 200 217 def calculate(self, namespace): 201 218 return self.expression.calculate(namespace) 202 219 203 class End Element:220 class End(_Element): 204 221 END = re.compile(r'^#end(.*)', re.I + re.S) 205 222 def __init__(self, text): 206 m = self.END.match(text) 207 if not m: raise NoMatch() 208 self.remaining_text = m.group(1) 209 210 class NullElement: 223 self.remaining_text, = self.match_or_reject(self.END, text) 224 225 class Null: 211 226 def evaluate(self, namespace, stream): pass 212 227 213 class If Element:228 class If(_Element): 214 229 START = re.compile(r'^#if\b\s*(.*)$', re.S + re.I) 215 230 START_ELSE = re.compile(r'^#else(.*)$', re.S + re.I) 216 else_block = NullElement() 217 218 def __init__(self, text): 219 m = self.START.match(text) 220 if not m: raise NoMatch() 221 text = m.group(1) 222 self.condition = ConditionElement(text) 223 text = self.condition.remaining_text 224 self.block = BlockElement(text) 225 text = self.block.remaining_text 231 else_block = Null() 232 233 def __init__(self, text): 234 text, = self.match_or_reject(self.START, text) 235 self.condition, text = self.next_element(Condition, text) 236 self.block, text = self.next_element(Block, text) 226 237 m = self.START_ELSE.match(text) 227 238 if m: 228 text = m.group(1) 229 self.else_block = BlockElement(text) 230 text = self.else_block.remaining_text 231 try: 232 end = EndElement(text) 233 self.remaining_text = end.remaining_text 234 except NoMatch: 235 raise TemplateSyntaxError(self, '#end', text) 239 self.else_block, text = self.require_next_element(Block, m.group(1), 'block') 240 end, self.remaining_text = self.require_next_element(End, text, '#end') 236 241 237 242 def evaluate(self, namespace, stream): … … 242 247 243 248 244 class Set Element:249 class Set(_Element): 245 250 START = re.compile(r'^#set\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S + re.I) 246 251 CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 247 252 def __init__(self, text): 248 m = self.START.match(text) 249 if not m: raise NoMatch() ## Could be cleaner b/c syntax error if no '(' 250 self.var_name, text = m.groups() 251 self.value = ValueElement(text) 252 text = self.value.remaining_text 253 m = self.CLOSING_PATTERN.match(text) 254 if not m: 255 raise TemplateSyntaxError(self, ')', text) 256 self.remaining_text = m.group(1) 253 ## Could be cleaner b/c syntax error if no '(' 254 self.var_name, text = self.match_or_reject(self.START, text) 255 self.value, text = self.next_element(Value, text) 256 self.remaining_text, = self.require_match(self.CLOSING_PATTERN, text, ')') 257 257 258 258 def evaluate(self, namespace, stream): … … 260 260 261 261 262 class Foreach Element:262 class Foreach(_Element): 263 263 START = re.compile(r'^#foreach\s*\(\s*\$([a-z_][a-z0-9_]*)\s*in\s*(.*)$', re.S + re.I) 264 264 CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 265 265 def __init__(self, text): 266 m = self.START.match(text) 267 if not m: raise NoMatch() ## Could be cleaner b/c syntax error if no '(' 268 self.loop_var_name, text = m.groups() 269 self.value = ValueElement(text) 270 text = self.value.remaining_text 271 m = self.CLOSING_PATTERN.match(text) 272 if not m: 273 raise TemplateSyntaxError(self, ')', text) 274 text = m.group(1) 275 self.block = BlockElement(text) 276 text = self.block.remaining_text 277 try: end = EndElement(text) 278 except NoMatch: raise TemplateSyntaxError(self, '#end', text) 279 self.remaining_text = end.remaining_text 280 266 ## Could be cleaner b/c syntax error if no '(' 267 self.loop_var_name, text = self.match_or_reject(self.START, text) 268 self.value, text = self.next_element(Value, text) 269 text, = self.require_match(self.CLOSING_PATTERN, text, ')') 270 self.block, text = self.next_element(Block, text) 271 end, self.remaining_text = self.require_next_element(End, text, '#end') 281 272 282 273 def evaluate(self, namespace, stream): … … 290 281 counter += 1 291 282 292 class TemplateElement: 293 def __init__(self, text): 294 self.block = BlockElement(text) 295 if self.block.remaining_text: 283 284 class TemplateBody(_Element): 285 def __init__(self, text): 286 self.block, text = self.next_element(Block, text) 287 if text: 296 288 raise TemplateSyntaxError(self, 'block element', self.block.remaining_text) 297 289 … … 301 293 302 294 303 class Block Element:295 class Block(_Element): 304 296 def __init__(self, text): 305 297 self.children = [] 306 298 while text: 307 child _matched = False308 for child_type in (Text Element, PlaceholderElement, CommentElement, IfElement, SetElement, ForeachElement):299 child = None 300 for child_type in (Text, Placeholder, Comment, If, Set, Foreach): 309 301 try: 310 child = child_type(text) 311 text = child.remaining_text 302 child, text = self.next_element(child_type, text) 312 303 self.children.append(child) 313 child_matched = True314 304 break 315 305 except NoMatch: 316 306 continue 317 if not child_matched:307 if child is None: 318 308 break 319 309 self.remaining_text = text … … 324 314 325 315 326 327 class Template:328 def __init__(self, content):329 self.content = content330 self.evaluator = None331 332 def merge(self, namespace):333 output = StringIO.StringIO()334 self.merge_to(namespace, output)335 return output.getvalue()336 337 def merge_to(self, namespace, fileobj):338 output = []339 if not self.evaluator:340 self.evaluator = TemplateElement(self.content)341 self.evaluator.evaluate(namespace, fileobj)trunk/airspeed_test.py
r16 r17 160 160 self.assertEquals(" false ", template.merge({})) 161 161 162 def test_too_many_end_clauses_trigger_error(self): 163 template = airspeed.Template('#if (1)true!#end #end ') 164 self.assertRaises(airspeed.TemplateSyntaxError, template.merge, {}) 165 166 def test_can_call_function_with_one_parameter(self): 167 def squared(number): 168 return number * number 169 template = airspeed.Template('$squared(8)') 170 self.assertEquals("64", template.merge(locals())) 171 some_var = 6 172 template = airspeed.Template('$squared($some_var)') 173 self.assertEquals("36", template.merge(locals())) 174 template = airspeed.Template('$squared($squared($some_var))') 175 self.assertEquals("1296", template.merge(locals())) 176 177 def test_can_call_function_with_one_parameter(self): 178 def multiply(number1, number2): 179 return number1 * number2 180 template = airspeed.Template('$multiply(2, 4)') 181 self.assertEquals("8", template.merge(locals())) 182 template = airspeed.Template('$multiply( 2 , 4 )') 183 self.assertEquals("8", template.merge(locals())) 184 value1, value2 = 4, 12 185 template = airspeed.Template('$multiply($value1,$value2)') 186 self.assertEquals("48", template.merge(locals())) 162 187 163 188 # … … 166 191 # Escaped characters in string literals 167 192 # Directives inside string literals 168 # #else , #elseif193 # #elseif 169 194 # Parameterised calls 170 195 # #parse, #include
