Changeset 17

Show
Ignore:
Timestamp:
17/08/04 20:21:00 (4 years ago)
Author:
steve
Message:

refactoring for brevity; parameterised function calls implemented

Files:

Legend:

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

    r16 r17  
    33import re 
    44import cStringIO as StringIO 
     5 
     6__all__ = ['TemplateSyntaxError', 'Template'] 
     7 
     8############################################################################### 
     9# Public interface 
     10############################################################################### 
     11 
     12class 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) 
    527 
    628 
     
    1234 
    1335 
     36############################################################################### 
     37# Internals 
     38############################################################################### 
     39 
    1440class NoMatch(Exception): pass 
    1541 
     
    1945        dict.__init__(self) 
    2046        self.parent = parent 
     47 
    2148    def __getitem__(self, key): 
    2249        try: return dict.__getitem__(self, key) 
     
    2451 
    2552 
    26 class TextElement: 
     53class _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 
     74class Text(_Element): 
    2775    MY_PATTERN = re.compile(r'^((?:[^\\\$#]|\\[\$#])+|\$[^!\{a-z0-9_]|\$$)(.*)$', re.S + re.I) 
    2876    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) 
    3278 
    3379    def evaluate(self, namespace, stream): 
     
    3581 
    3682 
    37 class IntegerLiteralElement
     83class IntegerLiteral(_Element)
    3884    MY_PATTERN = re.compile(r'^(\d+)(.*)', re.S) 
    3985    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) 
    4488 
    4589    def calculate(self, namespace): 
     
    4791 
    4892 
    49 class StringLiteralElement
     93class StringLiteral(_Element)
    5094    MY_PATTERN = re.compile(r'^"((?:\\["nrbt\\\\]|[^"\n\r"\\])+)"(.*)', re.S) 
    5195    ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])') 
    5296    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) 
    5598        def unescape(match): 
    5699            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) 
    59101 
    60102    def calculate(self, namespace): 
     
    62104 
    63105 
    64 class ValueElement
     106class Value(_Element)
    65107    def __init__(self, text): 
    66108        if text.startswith('$'): 
    67             self.expression = ExpressionElement(text[1:]) 
     109            self.expression = Expression(text[1:]) 
    68110        else: 
    69111            try: 
    70                 self.expression = IntegerLiteralElement(text) 
     112                self.expression = IntegerLiteral(text) 
    71113            except NoMatch: 
    72                 self.expression = StringLiteralElement(text) 
     114                self.expression = StringLiteral(text) 
    73115        self.remaining_text = self.expression.remaining_text 
    74116 
     
    77119 
    78120 
    79 class ExpressionElement
     121class Expression(_Element)
    80122    NAME_PATTERN = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S) 
    81123    def __init__(self, text): 
     
    91133 
    92134    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) 
    96136        parameter_list = None 
    97137        try: 
    98             parameter_list = ParameterListElement(text) 
    99             text = parameter_list.remaining_text 
     138            parameter_list, text = self.next_element(ParameterList, text) 
    100139        except NoMatch: 
    101140            pass 
     
    117156 
    118157 
    119 class ParameterListElement
     158class ParameterList(_Element)
    120159    OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 
    121160    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
     
    124163    def __init__(self, text): 
    125164        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: 
    134170            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 
     179class Placeholder(_Element): 
    141180    MY_PATTERN = re.compile(r'^\$(!?)(\{?)(.*)$', re.S) 
    142181    CLOSING_BRACE_PATTERN = re.compile(r'^\}(.*)$', re.S) 
    143182    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') 
    154185        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, '}') 
    159187        self.remaining_text = text 
    160188 
     
    170198 
    171199 
    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() 
     200class 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) 
    182204 
    183205    def evaluate(self, namespace, stream): 
     
    185207 
    186208 
    187 class ConditionElement
     209class Condition(_Element)
    188210    OPENING_PATTERN = re.compile(r'^\(\s*(.*)$', re.S) 
    189211    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
    190212    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, ')') 
    199216 
    200217    def calculate(self, namespace): 
    201218        return self.expression.calculate(namespace) 
    202219 
    203 class EndElement
     220class End(_Element)
    204221    END = re.compile(r'^#end(.*)', re.I + re.S) 
    205222    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 
     225class Null: 
    211226    def evaluate(self, namespace, stream): pass 
    212227 
    213 class IfElement
     228class If(_Element)
    214229    START = re.compile(r'^#if\b\s*(.*)$', re.S + re.I) 
    215230    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) 
    226237        m = self.START_ELSE.match(text) 
    227238        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') 
    236241 
    237242    def evaluate(self, namespace, stream): 
     
    242247 
    243248 
    244 class SetElement
     249class Set(_Element)
    245250    START = re.compile(r'^#set\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S + re.I) 
    246251    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
    247252    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, ')') 
    257257 
    258258    def evaluate(self, namespace, stream): 
     
    260260 
    261261 
    262 class ForeachElement
     262class Foreach(_Element)
    263263    START = re.compile(r'^#foreach\s*\(\s*\$([a-z_][a-z0-9_]*)\s*in\s*(.*)$', re.S + re.I) 
    264264    CLOSING_PATTERN = re.compile(r'^\s*\)(.*)$', re.S) 
    265265    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') 
    281272 
    282273    def evaluate(self, namespace, stream): 
     
    290281            counter += 1 
    291282 
    292 class TemplateElement: 
    293     def __init__(self, text): 
    294         self.block = BlockElement(text) 
    295         if self.block.remaining_text: 
     283 
     284class TemplateBody(_Element): 
     285    def __init__(self, text): 
     286        self.block, text = self.next_element(Block, text) 
     287        if text: 
    296288            raise TemplateSyntaxError(self, 'block element', self.block.remaining_text) 
    297289 
     
    301293 
    302294 
    303 class BlockElement
     295class Block(_Element)
    304296    def __init__(self, text): 
    305297        self.children = [] 
    306298        while text: 
    307             child_matched = Fals
    308             for child_type in (TextElement, PlaceholderElement, CommentElement, IfElement, SetElement, ForeachElement): 
     299            child = Non
     300            for child_type in (Text, Placeholder, Comment, If, Set, Foreach): 
    309301                try: 
    310                     child = child_type(text) 
    311                     text = child.remaining_text 
     302                    child, text = self.next_element(child_type, text) 
    312303                    self.children.append(child) 
    313                     child_matched = True 
    314304                    break 
    315305                except NoMatch: 
    316306                    continue 
    317             if not child_matched
     307            if child is None
    318308                break 
    319309        self.remaining_text = text 
     
    324314 
    325315 
    326  
    327 class Template: 
    328     def __init__(self, content): 
    329         self.content = content 
    330         self.evaluator = None 
    331  
    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  
    160160        self.assertEquals(" false ", template.merge({})) 
    161161 
     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())) 
    162187 
    163188# 
     
    166191#  Escaped characters in string literals 
    167192#  Directives inside string literals 
    168 #  #else, #elseif 
     193#  #elseif 
    169194#  Parameterised calls 
    170195#  #parse, #include