Changeset 14

Show
Ignore:
Timestamp:
17/08/04 17:41:58 (4 years ago)
Author:
steve
Message:

rewritten to use grammar elements with their own regexes

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/airspeed.py

    r13 r14  
    55 
    66 
    7 class TemplateSyntaxError(Exception): pass 
    8  
    9  
    10 """ 
    11 VARIABLE_NAME   ->   '[a-zA-Z]+' 
    12 TEXT            ->   '(?:[^\$#\\]|\\\\|\\\$|\\#)+' 
    13 TEMPLATE        ->   BLOCK 
    14 BLOCK           ->   TEXT 
    15                    | PLACEHOLDER 
    16                    | IF_DIRECTIVE 
    17                    | BLOCK_DIRECTIVE 
    18 REFERENCE        ->  '\$'  VARIABLE_VALUE 
    19 SILENT_REFERENCE ->  '\$!' VARIABLE_VALUE 
    20 VARIABLE_VALUE   ->  VARIABLE_NAME 
    21                    | VARIABLE_NAME '\.' VARIABLE_VALUE 
    22  
    23  
    24 """ 
    25  
    26  
    27  
    28 class Tokeniser: 
    29     PLAIN, IF, PLACEHOLDER, FOREACH, END, SET, ELSE = range(7) 
    30  
    31     UP_TO_NEXT_TEMPLATE_BIT = re.compile('^(.*?)((?:#|\$).*)', re.MULTILINE + re.DOTALL) 
    32     REST = '(.*)$' 
    33     NAME = '[a-z0-9_]+' 
    34     NAME_OR_CALL = NAME + '(?:\(\))?' 
    35     RE_FLAGS = re.IGNORECASE + re.DOTALL + re.MULTILINE 
    36     EXPRESSION = '(' + NAME_OR_CALL + '(?:\.' + NAME_OR_CALL + ')*)' 
    37     STRING_LITERAL = "'(?:\\\\|\\'|\\n|\\b|\\t)'" 
    38     PLACEHOLDER_PATTERN = re.compile('^\$(!?)({?)' + EXPRESSION + '(}?)' + REST, RE_FLAGS) 
    39     SET_PATTERN = re.compile('^#set[ \t]*\([ \t]*\$(' + NAME + ')[ \t]*=[ \t]*(\d+|"[^"]+")[ \t]*\)' + REST, RE_FLAGS) 
    40     BEGIN_IF_PATTERN = re.compile('^#if[ \t]*\([ \t]*\$' + EXPRESSION + '[ \t]*\)' + REST, RE_FLAGS) 
    41     BEGIN_FOREACH_PATTERN = re.compile('^#foreach[ \t]*\([ \t]*\$(' + NAME + ')[ \t]+in[ \t]+\$' + EXPRESSION + '[ \t]*\)' + REST, RE_FLAGS) 
    42     END_PATTERN = re.compile('^#end' + REST, RE_FLAGS) 
    43     ELSE_PATTERN = re.compile('^#else' + REST, RE_FLAGS) 
    44     COMMENT_PATTERN = re.compile('^##.*?(?:\n|$)' + REST, RE_FLAGS) 
    45     MULTI_LINE_COMMENT_PATTERN = re.compile('^#\*.*?\*#(?:[ \t]*\n)?' + REST, RE_FLAGS) 
    46  
    47     def tokenise(self, text): 
    48         while True: 
    49             m = self.UP_TO_NEXT_TEMPLATE_BIT.match(text) 
     7class TemplateSyntaxError(Exception): 
     8    def __init__(self, element, expected, got): 
     9        if len(got) > 40: 
     10            got = got[:36] + ' ...' 
     11        Exception.__init__(self,"%s: expected %s, got: %s ..." % (element.__class__.__name__, expected, got)) 
     12class NoMatch(Exception): pass 
     13 
     14 
     15class LocalNamespace(dict): 
     16    def __init__(self, parent): 
     17        dict.__init__(self) 
     18        self.parent = parent 
     19    def __getitem__(self, key): 
     20        try: return dict.__getitem__(self, key) 
     21        except KeyError: return self.parent[key] 
     22 
     23class TextElement: 
     24    MY_PATTERN = re.compile(r'^((?:[^\\\$#]|\\[\$#])+|\$[^!\{\}a-z0-9_])(.*)$', re.S + re.I) 
     25    def __init__(self, text): 
     26        m = self.MY_PATTERN.match(text) 
     27        if not m: raise NoMatch() 
     28        self.text, self.remaining_text = m.groups() 
     29 
     30    def evaluate(self, namespace, stream): 
     31        stream.write(self.text) 
     32 
     33 
     34class IntegerLiteralElement: 
     35    MY_PATTERN = re.compile(r'^(\d+)(.*)', re.S) 
     36    def __init__(self, text): 
     37        m = self.MY_PATTERN.match(text) 
     38        if not m: raise NoMatch() 
     39        self.value = int(m.group(1)) 
     40        self.remaining_text = m.group(2) 
     41 
     42    def calculate(self, namespace): 
     43        return self.value 
     44 
     45 
     46class StringLiteralElement: 
     47    MY_PATTERN = re.compile(r'^"((?:\\["nrbt\\]|[^"\n\r"\\])+)"(.*)', re.S) 
     48    ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])') 
     49    def __init__(self, text): 
     50        m = self.MY_PATTERN.match(text) 
     51        if not m: raise NoMatch() 
     52        def unescape(match): 
     53            return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\'}[match.group(1)] 
     54        self.value = self.ESCAPED_CHAR.sub(unescape, m.group(1)) 
     55        self.remaining_text = m.group(2) 
     56 
     57    def calculate(self, namespace): 
     58        return self.value 
     59 
     60 
     61class ValueElement: 
     62    def __init__(self, text): 
     63        if text.startswith('$'): 
     64            self.expression = ExpressionElement(text[1:]) 
     65        else: 
     66            try: 
     67                self.expression = IntegerLiteralElement(text) 
     68            except NoMatch: 
     69                self.expression = StringLiteralElement(text) 
     70        self.remaining_text = self.expression.remaining_text 
     71 
     72    def calculate(self, namespace): 
     73        return self.expression.calculate(namespace) 
     74 
     75 
     76class ExpressionElement: 
     77    NAME_PATTERN = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S) 
     78    def __init__(self, text): 
     79        self.names_and_calls = [] 
     80        try: text = self.read_name_or_call(text) 
     81        except NoMatch: raise TemplateSyntaxError(self, 'name or call', text) 
     82        while text.startswith('.'): 
     83            try: 
     84                text = self.read_name_or_call(text[1:]) 
     85            except NoMatch:   # for the '$name. blah' case 
     86                break 
     87        self.remaining_text = text 
     88 
     89    def read_name_or_call(self, text): 
     90        m = self.NAME_PATTERN.match(text) 
     91        if not m: raise NoMatch() 
     92        name, text = m.groups() 
     93        parameter_list = None 
     94        try: 
     95            parameter_list = ParameterListElement(text) 
     96            text = parameter_list.remaining_text 
     97        except NoMatch: 
     98            pass 
     99        self.names_and_calls.append((name, parameter_list)) 
     100        return text 
     101 
     102    def calculate(self, namespace): 
     103        result = namespace 
     104        for name, parameters in self.names_and_calls: 
     105            try: result = getattr(result, name) 
     106            except AttributeError: 
     107                try: result = result[name] 
     108                except KeyError: pass 
     109            if result in (None, namespace): return None ## TODO: an explicit 'not found' exception? 
     110            if parameters is not None: 
     111                values = [value.calculate(namespace) for value in parameters.values] 
     112                result = result(*values) 
     113        return result 
     114 
     115 
     116class ParameterListElement: 
     117    OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 
     118    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
     119    COMMA_PATTERN = re.compile(r'^\s*,\s*(.*)$', re.S) 
     120 
     121    def __init__(self, text): 
     122        self.values = [] 
     123        m = self.OPENING_PATTERN.match(text) 
     124        if not m: raise NoMatch() 
     125        text = m.group(1) 
     126        while True:   ## FIXME 
     127            m = self.COMMA_PATTERN.match(text) 
     128            if not m: break 
     129            value = ValueElement(m.group(1)) 
     130            text = value.remaining_text 
     131            self.values.append(value) 
     132        m = self.CLOSING_PATTERN.match(text) 
     133        if not m: raise TemplateSyntaxError(self, ')', text) 
     134        self.remaining_text = m.group(1) 
     135 
     136 
     137class PlaceholderElement: 
     138    MY_PATTERN = re.compile(r'^\$(!?)(\{?)(.*)$', re.S) 
     139    CLOSING_BRACE_PATTERN = re.compile(r'^\}(.*)$', re.S) 
     140    def __init__(self, text): 
     141        m = self.MY_PATTERN.match(text) 
     142        if not m: raise NoMatch() 
     143        self.silent = bool(m.group(1)) 
     144        self.braces = bool(m.group(2)) 
     145        text = m.group(3) 
     146        try: 
     147            self.expression = ExpressionElement(text) 
     148            text = self.expression.remaining_text 
     149        except NoMatch: 
     150            raise TemplateSyntaxError(self, 'expression', text) 
     151        if self.braces: 
     152            m = self.CLOSING_BRACE_PATTERN.match(text) 
    50153            if not m: 
    51                 yield self.PLAIN, text 
     154                raise TemplateSyntaxError(self, '}', text) 
     155            text = m.group(1) 
     156        self.remaining_text = text 
     157 
     158    def evaluate(self, namespace, stream): 
     159        value = self.expression.calculate(namespace) 
     160        if value is None: 
     161            if self.silent: value = '' 
     162            else: 
     163                value_as_str = '.'.join([name for name, params in self.expression.names_and_calls]) 
     164                if self.braces: value = '${%s}' % value_as_str 
     165                else: value = '$%s' % value_as_str 
     166        stream.write(str(value)) 
     167 
     168 
     169class CommentElement: 
     170    COMMENT_PATTERN = re.compile('^##.*?(?:\n|$)(.*)$', re.M + re.S) 
     171    MULTI_LINE_COMMENT_PATTERN = re.compile('^#\*.*?\*#(?:[ \t]*\n)?(.*$)', re.M + re.S) 
     172    def __init__(self, text): 
     173        for pattern in (self.COMMENT_PATTERN, self.MULTI_LINE_COMMENT_PATTERN): 
     174            m = pattern.match(text) 
     175            if not m: continue 
     176            self.remaining_text = m.group(1) 
     177            return 
     178        raise NoMatch() 
     179 
     180    def evaluate(self, namespace, stream): 
     181        pass 
     182 
     183 
     184class ConditionElement: 
     185    OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 
     186    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
     187    def __init__(self, text): 
     188        m = self.OPENING_PATTERN.match(text) 
     189        if not m: raise TemplateSyntaxError(self, '(', text) 
     190        text = m.group(1) 
     191        self.expression = ValueElement(text) 
     192        text = self.expression.remaining_text 
     193        m = self.CLOSING_PATTERN.match(text) 
     194        if not m: raise TemplateSyntaxError(self, ')', text) 
     195        self.remaining_text = m.group(1) 
     196 
     197    def calculate(self, namespace): 
     198        return self.expression.calculate(namespace) 
     199 
     200class EndElement: 
     201    END = re.compile(r'^#end(.*)', re.I + re.S) 
     202    def __init__(self, text): 
     203        m = self.END.match(text) 
     204        if not m: raise NoMatch() 
     205        self.remaining_text = m.group(1) 
     206 
     207 
     208class IfElement: 
     209    START = re.compile(r'^#if\b\s*(.*)', re.S + re.I) 
     210    def __init__(self, text): 
     211        m = self.START.match(text) 
     212        if not m: raise NoMatch() 
     213        text = m.group(1) 
     214        self.condition = ConditionElement(text) 
     215        text = self.condition.remaining_text 
     216        self.block = BlockElement(text) 
     217        text = self.block.remaining_text 
     218        try: 
     219            end = EndElement(text) 
     220            self.remaining_text = end.remaining_text 
     221        except NoMatch: 
     222            raise TemplateSyntaxError(self, '#end', text) 
     223 
     224    def evaluate(self, namespace, stream): 
     225        if self.condition.calculate(namespace): 
     226            self.block.evaluate(namespace, stream) 
     227 
     228 
     229class SetElement: 
     230    START = re.compile(r'^#set\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S + re.I) 
     231    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
     232    def __init__(self, text): 
     233        m = self.START.match(text) 
     234        if not m: raise NoMatch() ## Could be cleaner b/c syntax error if no '(' 
     235        self.var_name, text = m.groups() 
     236        self.value = ValueElement(text) 
     237        text = self.value.remaining_text 
     238        m = self.CLOSING_PATTERN.match(text) 
     239        if not m: 
     240            raise TemplateSyntaxError(self, ')', text) 
     241        self.remaining_text = m.group(1) 
     242 
     243    def evaluate(self, namespace, stream): 
     244        namespace[self.var_name] = self.value.calculate(namespace) 
     245 
     246 
     247class ForeachElement: 
     248    START = re.compile(r'^#foreach\s*\(\s*\$([a-z_][a-z0-9_]*)\s*in\s*(.*)$', re.S + re.I) 
     249    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
     250    def __init__(self, text): 
     251        m = self.START.match(text) 
     252        if not m: raise NoMatch() ## Could be cleaner b/c syntax error if no '(' 
     253        self.loop_var_name, text = m.groups() 
     254        self.value = ValueElement(text) 
     255        text = self.value.remaining_text 
     256        m = self.CLOSING_PATTERN.match(text) 
     257        if not m: 
     258            raise TemplateSyntaxError(self, ')', text) 
     259        text = m.group(1) 
     260        self.block = BlockElement(text) 
     261        text = self.block.remaining_text 
     262        try: end = EndElement(text) 
     263        except NoMatch: raise TemplateSyntaxError(self, '#end', text) 
     264        self.remaining_text = end.remaining_text 
     265 
     266 
     267    def evaluate(self, namespace, stream): 
     268        iterable = self.value.calculate(namespace) 
     269        counter = 1 
     270        for item in iterable: 
     271            namespace = LocalNamespace(namespace) 
     272            namespace['velocityCount'] = counter 
     273            namespace[self.loop_var_name] = item 
     274            self.block.evaluate(namespace, stream) 
     275            counter += 1 
     276 
     277class TemplateElement: 
     278    def __init__(self, text): 
     279        self.block = BlockElement(text) 
     280        if self.block.remaining_text: 
     281            raise TemplateSyntaxError(self, 'block element', self.block.remaining_text) 
     282 
     283    def evaluate(self, namespace, stream): 
     284        namespace = LocalNamespace(namespace) 
     285        self.block.evaluate(namespace, stream) 
     286 
     287 
     288class BlockElement: 
     289    def __init__(self, text): 
     290        self.children = [] 
     291        while text: 
     292            child_matched = False 
     293            for child_type in (TextElement, PlaceholderElement, CommentElement, IfElement, SetElement, ForeachElement): 
     294                try: 
     295                    child = child_type(text) 
     296                    text = child.remaining_text 
     297                    self.children.append(child) 
     298                    child_matched = True 
     299                    break 
     300                except NoMatch: 
     301                    continue 
     302            if not child_matched: 
    52303                break 
    53             plain, interesting = m.groups() 
    54             yield self.PLAIN, plain 
    55             m = self.PLACEHOLDER_PATTERN.match(interesting) 
    56             if m: 
    57                 expression, silent, original_text, text = self.get_placeholder(m) 
    58                 yield self.PLACEHOLDER, (expression, silent, original_text) 
    59                 continue 
    60             m = self.BEGIN_IF_PATTERN.match(interesting) 
    61             if m: 
    62                 expression, text = m.groups() 
    63                 yield self.IF, expression 
    64                 continue 
    65             m = self.BEGIN_FOREACH_PATTERN.match(interesting) 
    66             if m: 
    67                 iter_var, expression, text = m.groups() 
    68                 yield self.FOREACH, (expression, iter_var) 
    69                 continue 
    70             m = self.END_PATTERN.match(interesting) 
    71             if m: 
    72                 yield self.END, None 
    73                 (text,) = m.groups() 
    74                 continue 
    75             m = self.SET_PATTERN.match(interesting) 
    76             if m: 
    77                 (var_name, rvalue, text) = m.groups() 
    78                 yield self.SET, (var_name, rvalue) 
    79                 continue 
    80             m = self.ELSE_PATTERN.match(interesting) 
    81             if m: 
    82                 (text,) = m.groups() 
    83                 yield self.ELSE, None 
    84                 continue 
    85             m = self.COMMENT_PATTERN.match(interesting) 
    86             if m: 
    87                 (text,) = m.groups() 
    88                 continue 
    89             m = self.MULTI_LINE_COMMENT_PATTERN.match(interesting) 
    90             if m: 
    91                 (text,) = m.groups() 
    92                 continue 
    93             if interesting.startswith('$'): 
    94                 text = interesting[1:] 
    95                 yield self.PLAIN, '$' 
    96                 continue 
    97             raise TemplateSyntaxError("invalid token: %s" % text[:40]) 
    98  
    99     def get_placeholder(self, match): 
    100         silent, open_brace, var_name, close_brace, rest = match.groups() 
    101         if open_brace and not close_brace: 
    102             raise TemplateSyntaxError("unmatched braces") 
    103         if close_brace and not open_brace: 
    104             rest = close_brace + rest 
    105             original_text = ''.join(('$', silent, var_name)) 
    106         else: 
    107             original_text = ''.join(('$', open_brace, silent, var_name, close_brace)) 
    108         return var_name, bool(silent), original_text, rest 
    109  
    110  
    111  
    112 class Evaluator: 
    113     def eval_expression(self, expression, namespace_dict): 
    114         o = namespace_dict 
    115         for part in expression.split('.'): 
    116             if part.endswith('()'):  ## FIXME 
    117                 part = part[:-2] 
    118                 try: o = getattr(o, part) 
    119                 except AttributeError: pass 
    120                 else: o = o() 
    121             else: 
    122                 try: o = getattr(o, part) 
    123                 except AttributeError: 
    124                     try: o = o[part] 
    125                     except KeyError: pass 
    126             if o in (None, namespace_dict): return None 
    127         return o 
    128  
    129  
    130 class BlockEvaluator(Evaluator): 
    131     class LocalNamespace(dict): 
    132         def __init__(self, parent_namespace): 
    133             self.parent_namespace = parent_namespace 
    134         def __getitem__(self, key): 
    135             try: return dict.__getitem__(self, key) 
    136             except KeyError: return self.parent_namespace[key] 
    137  
    138     def __init__(self): 
    139         self.children = [] 
    140         self.delegate = None 
    141  
    142     def evaluate(self, output_stream, namespace): 
    143         self.evaluate_block(output_stream, BlockEvaluator.LocalNamespace(namespace)) 
    144  
    145     def evaluate_block(self, output_stream, namespace): 
     304        self.remaining_text = text 
     305 
     306    def evaluate(self, namespace, stream): 
    146307        for child in self.children: 
    147             child.evaluate(output_stream, namespace) 
    148  
    149     def add_evaluator(self, evaluator): 
    150         self.children.append(evaluator) 
    151         if hasattr(evaluator, 'add_evaluator'): 
    152             self.delegate = evaluator 
    153  
    154     def delegate_token(self, token_type, token_value): 
    155         if self.delegate: 
    156             if not self.delegate.feed(token_type, token_value): 
    157                 self.delegate = None 
    158             return True 
    159         return False 
    160  
    161     def feed(self, token_type, token_value): 
    162         if self.delegate_token(token_type, token_value): 
    163             return True 
    164         if token_type == Tokeniser.END: return False 
    165         elif token_type == Tokeniser.PLAIN: self.add_evaluator(PlainTextEvaluator(token_value)) 
    166         elif token_type == Tokeniser.PLACEHOLDER: self.add_evaluator(PlaceholderEvaluator(token_value)) 
    167         elif token_type == Tokeniser.FOREACH: self.add_evaluator(ForeachEvaluator(token_value)) 
    168         elif token_type == Tokeniser.IF: self.add_evaluator(IfEvaluator(token_value)) 
    169         elif token_type == Tokeniser.SET: self.add_evaluator(SetEvaluator(token_value)) 
    170         else: raise TemplateSyntaxError("illegal token in block: %s, %s" % (token_type, token_value)) 
    171         return True 
    172  
    173  
    174 class PlainTextEvaluator(Evaluator): 
    175     def __init__(self, text): 
    176         self.text = text 
    177  
    178     def evaluate(self, output_stream, namespace): 
    179         output_stream.write(self.text) 
    180  
    181  
    182 class PlaceholderEvaluator(Evaluator): 
    183     def __init__(self, token_value): 
    184         self.expression, self.silent, self.original_text = token_value 
    185  
    186     def evaluate(self, output_stream, namespace): 
    187         value = self.eval_expression(self.expression, namespace) 
    188         if value is None: 
    189             if self.silent: expression_value = '' 
    190             else: expression_value = self.original_text 
    191         else: 
    192             expression_value = str(value) 
    193         output_stream.write(expression_value) 
    194  
    195  
    196 class IfEvaluator(BlockEvaluator): 
    197     def __init__(self, token_value): 
    198         BlockEvaluator.__init__(self) 
    199         self.condition_expression = token_value 
    200  
    201     def evaluate_block(self, output_stream, namespace): 
    202         value = self.eval_expression(self.condition_expression, namespace) 
    203         if value: 
    204             BlockEvaluator.evaluate_block(self, output_stream, namespace) 
    205  
    206  
    207 class ForeachEvaluator(BlockEvaluator): 
    208     def __init__(self, token_value): 
    209         BlockEvaluator.__init__(self) 
    210         self.expression, self.iter_var = token_value 
    211  
    212     def evaluate_block(self, output_stream, namespace): 
    213         values = self.eval_expression(self.expression, namespace) 
    214         counter = 1 
    215         for value in values: 
    216             namespace[self.iter_var] = value 
    217             namespace['velocityCount'] = counter 
    218             BlockEvaluator.evaluate_block(self, output_stream, namespace) 
    219             counter += 1 
    220  
    221  
    222 class SetEvaluator(Evaluator): 
    223     def __init__(self, token_value): 
    224         self.var_name, self.rvalue = token_value 
    225  
    226     def evaluate(self, output_stream, namespace): 
    227         if self.rvalue.startswith('"'): 
    228             value = self.rvalue[1:-1] 
    229         else: 
    230             value = int(self.rvalue) 
    231         namespace[self.var_name] = value 
     308            child.evaluate(namespace, stream) 
     309 
    232310 
    233311 
     
    245323        output = [] 
    246324        if not self.evaluator: 
    247             self.evaluator = BlockEvaluator() 
    248             for token_type, token_value in Tokeniser().tokenise(self.content): 
    249                 self.evaluator.feed(token_type, token_value) 
    250         self.evaluator.evaluate(fileobj, namespace) 
     325            self.evaluator = TemplateElement(self.content) 
     326        self.evaluator.evaluate(namespace, fileobj) 
  • trunk/airspeed_test.py

    r13 r14  
    146146        self.assertEquals('Hello Chris!', output.getvalue()) 
    147147 
     148    def test_string_literal_can_contain_embedded_escaped_quotes(self): 
     149        template = airspeed.Template('#set ($name = "\\"batman\\"")$name') 
     150        self.assertEquals('"batman"', template.merge({})) 
     151 
    148152#    def test_else_block_evaluated_if_if_expression_false(self): 
    149153#        template = airspeed.Template('#if ($value) true #else false #end') 
     
    163167#  Escaped $, # 
    164168#  Sub-object assignment:  #set( $customer.Behavior = $primate ) 
     169#  Q. What is scope of #set ($customer.Name = 'john')  ??? 
    165170# 
    166171