Changeset 21
- Timestamp:
- 18/08/04 18:08:26 (4 years ago)
- Files:
-
- trunk/airspeed.py (modified) (17 diffs)
- trunk/airspeed_test.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/airspeed.py
r20 r21 13 13 def __init__(self, content): 14 14 self.content = content 15 self. evaluator= None15 self.root_element = None 16 16 17 17 def merge(self, namespace): … … 21 21 22 22 def merge_to(self, namespace, fileobj): 23 if not self. evaluator:24 self. evaluator= TemplateBody(self.content)25 self. evaluator.evaluate(namespace, fileobj)23 if not self.root_element: 24 self.root_element = TemplateBody(self.content) 25 self.root_element.evaluate(namespace, fileobj) 26 26 27 27 28 28 class TemplateSyntaxError(Exception): 29 def __init__(self, element, expected, got): 29 line = 0 30 def __init__(self, element, expected): 31 self.element = element 32 self.text_understood = element.full_text()[:element.end] 33 self.line = 1 + self.text_understood.count('\n') 34 self.column = len(self.text_understood) - self.text_understood.rfind('\n') 35 got = element.next_text() 30 36 if len(got) > 40: 31 37 got = got[:36] + ' ...' 32 Exception.__init__(self,"%s: expected %s, got: %s ..." % (element.__class__.__name__, expected, got)) 38 Exception.__init__(self, "line %d, column %d: expected %s, got: %s ..." % (self.line, self.column, expected, got)) 39 40 def get_position_strings(self): 41 error_line_start = 1 + self.text_understood.rfind('\n') 42 if '\n' in self.element.next_text(): 43 error_line_end = self.element.next_text().find('\n') + self.element.end 44 else: 45 error_line_end = len(self.element.full_text()) 46 error_line = self.element.full_text()[error_line_start:error_line_end] 47 caret_pos = self.column 48 return [error_line, ' ' * (caret_pos - 1) + '^'] 33 49 34 50 … … 54 70 55 71 class _Element: 56 def identity_match(self, pattern, text): 57 m = pattern.match(text) 72 def __init__(self, text, start=0): 73 self._full_text = text 74 self.start = self.end = start 75 self.parse() 76 77 def next_text(self): 78 return self._full_text[self.end:] 79 80 def my_text(self): 81 return self._full_text[self.start:self.end] 82 83 def full_text(self): 84 return self._full_text 85 86 def syntax_error(self, expected): 87 return TemplateSyntaxError(self, expected) 88 89 def identity_match(self, pattern): 90 m = pattern.match(self._full_text, self.end) 58 91 if not m: raise NoMatch() 59 return m.groups() 60 61 def require_match(self, pattern, text, expected): 62 m = pattern.match(text) 63 if not m: raise TemplateSyntaxError(self, expected, text) 64 return m.groups() 65 66 def next_element(self, element_spec, text): 92 self.end = m.start(pattern.groups) 93 return m.groups()[:-1] 94 95 def optional_match(self, pattern): 96 m = pattern.match(self._full_text, self.end) 97 if not m: return False 98 self.end = m.start(pattern.groups) 99 return True 100 101 def require_match(self, pattern, expected): 102 m = pattern.match(self._full_text, self.end) 103 if not m: raise self.syntax_error(expected) 104 self.end = m.start(pattern.groups) 105 return m.groups()[:-1] 106 107 def next_element(self, element_spec): 67 108 if callable(element_spec): 68 element = element_spec(text) 69 return element, element.remaining_text 109 element = element_spec(self._full_text, self.end) 110 self.end = element.end 111 return element 70 112 else: 71 113 for element_class in element_spec: 72 try: element = element_class( text)114 try: element = element_class(self._full_text, self.end) 73 115 except NoMatch: pass 74 else: return element, element.remaining_text 116 else: 117 self.end = element.end 118 return element 75 119 raise NoMatch() 76 120 77 def require_next_element(self, element_spec, text,expected):121 def require_next_element(self, element_spec, expected): 78 122 if callable(element_spec): 79 try: element = element_spec(text) 80 except NoMatch: raise TemplateSyntaxError(self, expected, text) 81 else: return element, element.remaining_text 123 try: element = element_spec(self._full_text, self.end) 124 except NoMatch: raise self.syntax_error(expected) 125 else: 126 self.end = element.end 127 return element 82 128 else: 83 129 for element_class in element_spec: 84 try: element = element_class( text)130 try: element = element_class(self._full_text, self.end) 85 131 except NoMatch: pass 86 else: return element, element.remaining_text 132 else: 133 self.end = element.end 134 return element 87 135 expected = ', '.join([cls.__name__ for cls in element_spec]) 88 raise TemplateSyntaxError(self, 'one of: ' + expected, text)136 raise self.syntax_error(self, 'one of: ' + expected) 89 137 90 138 91 139 class Text(_Element): 92 MY_PATTERN = re.compile(r' ^((?:[^\\\$#]|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|\\\\)(.*)$', re.S + re.I)140 MY_PATTERN = re.compile(r'((?:[^\\\$#]|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|\\\\)(.*)$', re.S + re.I) 93 141 ESCAPED_CHAR = re.compile(r'\\([\\\$#])') 94 def __init__(self, text):95 text, self.remaining_text = self.identity_match(self.MY_PATTERN, text)142 def parse(self): 143 text, = self.identity_match(self.MY_PATTERN) 96 144 def unescape(match): 97 145 return match.group(1) … … 103 151 104 152 class IntegerLiteral(_Element): 105 MY_PATTERN = re.compile(r' ^(\d+)(.*)', re.S)106 def __init__(self, text):107 self.value, self.remaining_text = self.identity_match(self.MY_PATTERN, text)153 MY_PATTERN = re.compile(r'(\d+)(.*)', re.S) 154 def parse(self): 155 self.value, = self.identity_match(self.MY_PATTERN) 108 156 self.value = int(self.value) 109 157 … … 113 161 114 162 class StringLiteral(_Element): 115 MY_PATTERN = re.compile(r' ^"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S)163 MY_PATTERN = re.compile(r'"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S) 116 164 ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])') 117 def __init__(self, text):118 value, self.remaining_text = self.identity_match(self.MY_PATTERN, text)165 def parse(self): 166 value, = self.identity_match(self.MY_PATTERN) 119 167 def unescape(match): 120 168 return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\'}[match.group(1)] … … 126 174 127 175 class Value(_Element): 128 def __init__(self, text):129 self.expression , self.remaining_text = self.next_element((PlainReference, IntegerLiteral, StringLiteral), text)176 def parse(self): 177 self.expression = self.next_element((SimpleReference, IntegerLiteral, StringLiteral)) 130 178 131 179 def calculate(self, namespace): … … 134 182 135 183 class NameOrCall(_Element): 136 NAME_PATTERN = re.compile(r' ^([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S)184 NAME_PATTERN = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S) 137 185 parameters = None 138 def __init__(self, text):139 self.name, text = self.identity_match(self.NAME_PATTERN, text)140 try: self.parameters , text = self.next_element(ParameterList, text)186 def parse(self): 187 self.name, = self.identity_match(self.NAME_PATTERN) 188 try: self.parameters = self.next_element(ParameterList) 141 189 except NoMatch: pass 142 self.remaining_text = text143 190 144 191 def calculate(self, namespace, top_namespace): … … 156 203 157 204 class Expression(_Element): 158 def __init__(self, text):159 self.names_and_calls = []160 part, text = self.require_next_element(NameOrCall, text, 'name')161 self. names_and_calls.append(part)162 while text.startswith('.'):205 DOT = re.compile('\.(.*)', re.S) 206 def parse(self): 207 self.parts = [] 208 self.parts.append(self.require_next_element(NameOrCall, 'name')) 209 while self.optional_match(self.DOT): 163 210 try: 164 part, text = self.next_element(NameOrCall, text[1:])165 self.names_and_calls.append(part)166 except NoMatch: break # for the '$name. blah' case167 self.remaining_text = text211 self.parts.append(self.next_element(NameOrCall)) 212 except NoMatch: 213 self.end -= 1 ### HACK 214 break # for the '$name. blah' case 168 215 169 216 def calculate(self, namespace): 170 217 value = namespace 171 for part in self. names_and_calls:218 for part in self.parts: 172 219 value = part.calculate(value, namespace) 173 220 if value is None: return None … … 176 223 177 224 class ParameterList(_Element): 178 OPENING_PATTERN = re.compile(r' ^\(\s*(.*)$', re.S)179 CLOSING_PATTERN = re.compile(r' ^\s*\)(.*)$', re.S)180 COMMA_PATTERN = re.compile(r' ^\s*,\s*(.*)$', re.S)181 182 def __init__(self, text):225 OPENING_PATTERN = re.compile(r'\(\s*(.*)$', re.S) 226 CLOSING_PATTERN = re.compile(r'\s*\)(.*)$', re.S) 227 COMMA_PATTERN = re.compile(r'\s*,\s*(.*)$', re.S) 228 229 def parse(self): 183 230 self.values = [] 184 text, = self.identity_match(self.OPENING_PATTERN, text)185 try: value , text = self.next_element(Value, text)231 self.identity_match(self.OPENING_PATTERN) 232 try: value = self.next_element(Value) 186 233 except NoMatch: 187 234 pass 188 235 else: 189 236 self.values.append(value) 190 while True: 191 m = self.COMMA_PATTERN.match(text) 192 if not m: break 193 value, text = self.require_next_element(Value, m.group(1), 'value') 237 while self.optional_match(self.COMMA_PATTERN): 238 value = self.require_next_element(Value, 'value') 194 239 self.values.append(value) 195 self.re maining_text, = self.require_match(self.CLOSING_PATTERN, text, ')')240 self.require_match(self.CLOSING_PATTERN, ')') 196 241 197 242 198 243 class Placeholder(_Element): 199 MY_PATTERN = re.compile(r'^\$(!?)(\{?)(.*)$', re.S) 200 CLOSING_BRACE_PATTERN = re.compile(r'^\}(.*)$', re.S) 201 def __init__(self, text): 202 self.silent, self.braces, text = self.identity_match(self.MY_PATTERN, text) 203 self.expression, text = self.require_next_element(Expression, text, 'expression') 204 if self.braces: 205 text, = self.require_match(self.CLOSING_BRACE_PATTERN, text, '}') 206 self.remaining_text = text 244 MY_PATTERN = re.compile(r'\$(!?)(\{?)(.*)$', re.S) 245 CLOSING_BRACE_PATTERN = re.compile(r'\}(.*)$', re.S) 246 def parse(self): 247 self.silent, self.braces = self.identity_match(self.MY_PATTERN) 248 self.expression = self.require_next_element(Expression, 'expression') 249 if self.braces: self.require_match(self.CLOSING_BRACE_PATTERN, '}') 207 250 208 251 def evaluate(self, namespace, stream): … … 210 253 if value is None: 211 254 if self.silent: value = '' 212 else: 213 value_as_str = '.'.join([name.name for name in self.expression.names_and_calls]) 214 if self.braces: value = '${%s}' % value_as_str 215 else: value = '$%s' % value_as_str 255 else: value = self.my_text() 216 256 stream.write(str(value)) 217 257 218 258 219 class PlainReference(_Element): 220 def __init__(self, text): 221 if not text.startswith('$'): raise NoMatch() 222 self.expression, self.remaining_text = self.require_next_element(Expression, text[1:], 'name') 259 class SimpleReference(_Element): 260 LEADING_DOLLAR = re.compile('\$(.*)', re.S) 261 def parse(self): 262 self.identity_match(self.LEADING_DOLLAR) 263 self.expression = self.require_next_element(Expression, 'name') 223 264 self.calculate = self.expression.calculate 224 265 … … 229 270 230 271 class Comment(_Element, Null): 231 COMMENT_PATTERN = re.compile(' ^#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S)232 def __init__(self, text):233 self. remaining_text, = self.identity_match(self.COMMENT_PATTERN, text)272 COMMENT_PATTERN = re.compile('#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S) 273 def parse(self): 274 self.identity_match(self.COMMENT_PATTERN) 234 275 235 276 236 277 class Condition(_Element): 237 OPENING_PATTERN = re.compile(r' ^\(\s*(.*)$', re.S)238 CLOSING_PATTERN = re.compile(r' ^\s*\)(.*)$', re.S)239 def __init__(self, text):240 text, = self.require_match(self.OPENING_PATTERN, text, '(')241 self.expression , text = self.next_element(Value, text)242 self.re maining_text, = self.require_match(self.CLOSING_PATTERN, text, ')')278 OPENING_PATTERN = re.compile(r'\(\s*(.*)$', re.S) 279 CLOSING_PATTERN = re.compile(r'\s*\)(.*)$', re.S) 280 def parse(self): 281 self.require_match(self.OPENING_PATTERN, '(') 282 self.expression = self.next_element(Value) 283 self.require_match(self.CLOSING_PATTERN, ')') 243 284 self.calculate = self.expression.calculate 244 285 245 286 246 287 class End(_Element): 247 END = re.compile(r' ^#end(.*)', re.I + re.S)248 def __init__(self, text):249 self. remaining_text, = self.identity_match(self.END, text)288 END = re.compile(r'#end(.*)', re.I + re.S) 289 def parse(self): 290 self.identity_match(self.END) 250 291 251 292 252 293 class ElseBlock(_Element): 253 START = re.compile(r' ^#else(.*)$', re.S + re.I)254 def __init__(self, text):255 text, = self.identity_match(self.START, text)256 self.block , self.remaining_text = self.require_next_element(Block, text, 'block')294 START = re.compile(r'#else(.*)$', re.S + re.I) 295 def parse(self): 296 self.identity_match(self.START) 297 self.block = self.require_next_element(Block, 'block') 257 298 self.evaluate = self.block.evaluate 258 299 259 300 260 301 class ElseifBlock(_Element): 261 START = re.compile(r' ^#elseif\b\s*(.*)$', re.S + re.I)262 def __init__(self, text):263 text, = self.identity_match(self.START, text)264 self.condition , text = self.require_next_element(Condition, text, 'condition')265 self.block , self.remaining_text = self.require_next_element(Block, text, 'block')302 START = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I) 303 def parse(self): 304 self.identity_match(self.START) 305 self.condition = self.require_next_element(Condition, 'condition') 306 self.block = self.require_next_element(Block, 'block') 266 307 self.calculate = self.condition.calculate 267 308 self.evaluate = self.block.evaluate … … 269 310 270 311 class IfDirective(_Element): 271 START = re.compile(r' ^#if\b\s*(.*)$', re.S + re.I)272 START_ELSEIF = re.compile(r' ^#elseif\b\s*(.*)$', re.S + re.I)312 START = re.compile(r'#if\b\s*(.*)$', re.S + re.I) 313 START_ELSEIF = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I) 273 314 else_block = Null() 274 315 275 def __init__(self, text):276 text, = self.identity_match(self.START, text)277 self.condition , text = self.next_element(Condition, text)278 self.block , text = self.next_element(Block, text)316 def parse(self): 317 self.identity_match(self.START) 318 self.condition = self.next_element(Condition) 319 self.block = self.next_element(Block) 279 320 self.elseifs = [] 280 321 while True: 281 322 try: 282 elseif_block , text = self.next_element(ElseifBlock, text)323 elseif_block = self.next_element(ElseifBlock) 283 324 self.elseifs.append(elseif_block) 284 325 except NoMatch: 285 326 break 286 try: self.else_block , text = self.next_element(ElseBlock, text)327 try: self.else_block = self.next_element(ElseBlock) 287 328 except NoMatch: pass 288 end , self.remaining_text = self.require_next_element(End, text, '#else, #elseif or #end')329 end = self.require_next_element(End, '#else, #elseif or #end') 289 330 290 331 def evaluate(self, namespace, stream): … … 300 341 301 342 class Assignment(_Element): 302 START = re.compile(r' ^\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S)303 CLOSING_PATTERN = re.compile(r' ^\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M)304 def __init__(self, text):305 self.var_name, text = self.identity_match(self.START, text)306 self.value , text = self.next_element(Value, text)307 self.re maining_text, = self.require_match(self.CLOSING_PATTERN, text, ')')343 START = re.compile(r'\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S) 344 CLOSING_PATTERN = re.compile(r'\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M) 345 def parse(self): 346 self.var_name, = self.identity_match(self.START) 347 self.value = self.next_element(Value) 348 self.require_match(self.CLOSING_PATTERN, ')') 308 349 309 350 def calculate(self, namespace): … … 312 353 313 354 class SetDirective(_Element): 314 START = re.compile(r' ^#set\b(.*)', re.S + re.I)315 def __init__(self, text):316 text, = self.identity_match(self.START, text)317 self.assignment , self.remaining_text = self.require_next_element(Assignment, text, 'assignment')355 START = re.compile(r'#set\b(.*)', re.S + re.I) 356 def parse(self): 357 self.identity_match(self.START) 358 self.assignment = self.require_next_element(Assignment, 'assignment') 318 359 319 360 def evaluate(self, namespace, stream): … … 322 363 323 364 class ForeachDirective(_Element): 324 START = re.compile(r' ^#foreach\s*\(\s*\$([a-z_][a-z0-9_]*)\s*in\s*(.*)$', re.S + re.I)325 CLOSING_PATTERN = re.compile(r' ^\s*\)(.*)$', re.S)326 def __init__(self, text):365 START = re.compile(r'#foreach\s*\(\s*\$([a-z_][a-z0-9_]*)\s*in\s*(.*)$', re.S + re.I) 366 CLOSING_PATTERN = re.compile(r'\s*\)(.*)$', re.S) 367 def parse(self): 327 368 ## Could be cleaner b/c syntax error if no '(' 328 self.loop_var_name, text = self.identity_match(self.START, text)329 self.value , text = self.next_element(Value, text)330 text, = self.require_match(self.CLOSING_PATTERN, text, ')')331 self.block , text = self.next_element(Block, text)332 end, self.remaining_text = self.require_next_element(End, text, '#end')369 self.loop_var_name, = self.identity_match(self.START) 370 self.value = self.next_element(Value) 371 self.require_match(self.CLOSING_PATTERN, ')') 372 self.block = self.next_element(Block) 373 self.require_next_element(End, '#end') 333 374 334 375 def evaluate(self, namespace, stream): … … 344 385 345 386 class TemplateBody(_Element): 346 def __init__(self, text):347 self.block , text = self.next_element(Block, text)348 if text:349 raise TemplateSyntaxError(self, 'block element', self.block.remaining_text)387 def parse(self): 388 self.block = self.next_element(Block) 389 if self.next_text(): 390 raise self.syntax_error('block element') 350 391 351 392 def evaluate(self, namespace, stream): … … 355 396 356 397 class Block(_Element): 357 def __init__(self, text):398 def parse(self): 358 399 self.children = [] 359 while text:400 while True: 360 401 try: 361 child, text = self.next_element((Text, Placeholder, Comment, IfDirective, SetDirective, ForeachDirective), text) 362 self.children.append(child) 402 self.children.append(self.next_element((Text, Placeholder, Comment, IfDirective, SetDirective, ForeachDirective))) 363 403 except NoMatch: 364 404 break 365 self.remaining_text = text366 405 367 406 def evaluate(self, namespace, stream): 368 407 for child in self.children: 369 408 child.evaluate(namespace, stream) 409 trunk/airspeed_test.py
r19 r21 234 234 value1, value2 = False, False 235 235 self.assertEquals(' three ', template.merge(locals())) 236 237 def test_syntax_error_contains_line_and_column_pos(self): 238 try: airspeed.Template('#if ( $hello )\n\n#elseif blah').merge({}) 239 except airspeed.TemplateSyntaxError, e: 240 self.assertEquals((3, 9), (e.line, e.column)) 241 else: self.fail('expected error') 242 try: airspeed.Template('#else blah').merge({}) 243 except airspeed.TemplateSyntaxError, e: 244 self.assertEquals((1, 1), (e.line, e.column)) 245 else: self.fail('expected error') 246 247 def test_get_position_strings_in_syntax_error(self): 248 try: airspeed.Template('#else whatever').merge({}) 249 except airspeed.TemplateSyntaxError, e: 250 self.assertEquals(['#else whatever', 251 '^'], e.get_position_strings()) 252 else: self.fail('expected error') 253 254 def test_get_position_strings_in_syntax_error_when_newline_after_error(self): 255 try: airspeed.Template('#else whatever\n').merge({}) 256 except airspeed.TemplateSyntaxError, e: 257 self.assertEquals(['#else whatever', 258 '^'], e.get_position_strings()) 259 else: self.fail('expected error') 260 261 def test_get_position_strings_in_syntax_error_when_newline_before_error(self): 262 try: airspeed.Template('foobar\n #else whatever\n').merge({}) 263 except airspeed.TemplateSyntaxError, e: 264 self.assertEquals([' #else whatever', 265 ' ^'], e.get_position_strings()) 266 else: self.fail('expected error') 267 268 236 269 # 237 270 # TODO:
