Changeset 21 for trunk

Show
Ignore:
Timestamp:
18/08/04 18:08:26 (8 years ago)
Author:
steve
Message:

avoid passing text fragments between elements: pass positions instead. make TemplateSyntaxError? aware of error positions

Location:
trunk
Files:
2 modified

Legend:

Unmodified
Added
Removed
  • trunk/airspeed.py

    r20 r21  
    1313    def __init__(self, content): 
    1414        self.content = content 
    15         self.evaluator = None 
     15        self.root_element = None 
    1616 
    1717    def merge(self, namespace): 
     
    2121 
    2222    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) 
    2626 
    2727 
    2828class 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() 
    3036        if len(got) > 40: 
    3137            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) + '^'] 
    3349 
    3450 
     
    5470 
    5571class _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) 
    5891        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): 
    67108        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 
    70112        else: 
    71113            for element_class in element_spec: 
    72                 try: element = element_class(text) 
     114                try: element = element_class(self._full_text, self.end) 
    73115                except NoMatch: pass 
    74                 else: return element, element.remaining_text 
     116                else: 
     117                    self.end = element.end 
     118                    return element 
    75119            raise NoMatch() 
    76120 
    77     def require_next_element(self, element_spec, text, expected): 
     121    def require_next_element(self, element_spec, expected): 
    78122        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 
    82128        else: 
    83129            for element_class in element_spec: 
    84                 try: element = element_class(text) 
     130                try: element = element_class(self._full_text, self.end) 
    85131                except NoMatch: pass 
    86                 else: return element, element.remaining_text 
     132                else: 
     133                    self.end = element.end 
     134                    return element 
    87135            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) 
    89137 
    90138 
    91139class 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) 
    93141    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) 
    96144        def unescape(match): 
    97145            return match.group(1) 
     
    103151 
    104152class 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) 
    108156        self.value = int(self.value) 
    109157 
     
    113161 
    114162class StringLiteral(_Element): 
    115     MY_PATTERN = re.compile(r'^"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S) 
     163    MY_PATTERN = re.compile(r'"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S) 
    116164    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) 
    119167        def unescape(match): 
    120168            return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\'}[match.group(1)] 
     
    126174 
    127175class 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)) 
    130178 
    131179    def calculate(self, namespace): 
     
    134182 
    135183class 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) 
    137185    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) 
    141189        except NoMatch: pass 
    142         self.remaining_text = text 
    143190 
    144191    def calculate(self, namespace, top_namespace): 
     
    156203 
    157204class 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): 
    163210            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' case 
    167         self.remaining_text = text 
     211                self.parts.append(self.next_element(NameOrCall)) 
     212            except NoMatch: 
     213                self.end -= 1  ### HACK 
     214                break  # for the '$name. blah' case 
    168215 
    169216    def calculate(self, namespace): 
    170217        value = namespace 
    171         for part in self.names_and_calls: 
     218        for part in self.parts: 
    172219            value = part.calculate(value, namespace) 
    173220            if value is None: return None 
     
    176223 
    177224class 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): 
    183230        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) 
    186233        except NoMatch: 
    187234            pass 
    188235        else: 
    189236            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') 
    194239                self.values.append(value) 
    195         self.remaining_text, = self.require_match(self.CLOSING_PATTERN, text, ')') 
     240        self.require_match(self.CLOSING_PATTERN, ')') 
    196241 
    197242 
    198243class 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, '}') 
    207250 
    208251    def evaluate(self, namespace, stream): 
     
    210253        if value is None: 
    211254            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() 
    216256        stream.write(str(value)) 
    217257 
    218258 
    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') 
     259class 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') 
    223264        self.calculate = self.expression.calculate 
    224265 
     
    229270 
    230271class 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) 
    234275 
    235276 
    236277class 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.remaining_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, ')') 
    243284        self.calculate = self.expression.calculate 
    244285 
    245286 
    246287class 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) 
    250291 
    251292 
    252293class 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') 
    257298        self.evaluate = self.block.evaluate 
    258299 
    259300 
    260301class 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') 
    266307        self.calculate = self.condition.calculate 
    267308        self.evaluate = self.block.evaluate 
     
    269310 
    270311class 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) 
    273314    else_block = Null() 
    274315 
    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) 
    279320        self.elseifs = [] 
    280321        while True: 
    281322            try: 
    282                 elseif_block, text = self.next_element(ElseifBlock, text) 
     323                elseif_block = self.next_element(ElseifBlock) 
    283324                self.elseifs.append(elseif_block) 
    284325            except NoMatch: 
    285326                break 
    286         try: self.else_block, text = self.next_element(ElseBlock, text) 
     327        try: self.else_block = self.next_element(ElseBlock) 
    287328        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') 
    289330 
    290331    def evaluate(self, namespace, stream): 
     
    300341 
    301342class 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.remaining_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, ')') 
    308349 
    309350    def calculate(self, namespace): 
     
    312353 
    313354class 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') 
    318359 
    319360    def evaluate(self, namespace, stream): 
     
    322363 
    323364class 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): 
    327368        ## 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') 
    333374 
    334375    def evaluate(self, namespace, stream): 
     
    344385 
    345386class 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') 
    350391 
    351392    def evaluate(self, namespace, stream): 
     
    355396 
    356397class Block(_Element): 
    357     def __init__(self, text): 
     398    def parse(self): 
    358399        self.children = [] 
    359         while text: 
     400        while True: 
    360401            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))) 
    363403            except NoMatch: 
    364404                break 
    365         self.remaining_text = text 
    366405 
    367406    def evaluate(self, namespace, stream): 
    368407        for child in self.children: 
    369408            child.evaluate(namespace, stream) 
     409 
  • trunk/airspeed_test.py

    r19 r21  
    234234        value1, value2 = False, False 
    235235        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 
    236269# 
    237270# TODO: