|
1 # Copyright (c) 2005 Nokia Corporation |
|
2 # |
|
3 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
4 # you may not use this file except in compliance with the License. |
|
5 # You may obtain a copy of the License at |
|
6 # |
|
7 # http://www.apache.org/licenses/LICENSE-2.0 |
|
8 # |
|
9 # Unless required by applicable law or agreed to in writing, software |
|
10 # distributed under the License is distributed on an "AS IS" BASIS, |
|
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
12 # See the License for the specific language governing permissions and |
|
13 # limitations under the License. |
|
14 |
|
15 from StringIO import StringIO |
|
16 import re |
|
17 import os.path |
|
18 import traceback |
|
19 import sys |
|
20 |
|
21 TEMPLATEFILE_SUFFIX='.in' |
|
22 |
|
23 def is_templatefile(filename): |
|
24 return filename.endswith(TEMPLATEFILE_SUFFIX) |
|
25 |
|
26 def outfilename_from_infilename(infilename): |
|
27 if not is_templatefile(infilename): |
|
28 raise ValueError, "expected file name with suffix "+TEMPLATEFILE_SUFFIX+", got "+infilename |
|
29 return infilename[:-len(TEMPLATEFILE_SUFFIX)] |
|
30 |
|
31 class MacroEvaluationError(Exception): pass |
|
32 |
|
33 macro_delimiter_re=re.compile(r'(\${{|}})') |
|
34 macro_eval_re=re.compile('\${{(.*?)}}$',re.S) |
|
35 macro_exec_re=re.compile('\${{!(\s*\n)?(.*?)\s*}}$',re.S) |
|
36 macro_if_re =re.compile('\${{if (.*)}}$',re.S) |
|
37 |
|
38 def process_macros(instr, namespace=None): |
|
39 """Return result of doing macro processing on instr, in namespace namespace. |
|
40 |
|
41 Basic substitution and evaluation: |
|
42 >>> process_macros('${{foo}}',{'foo':1}) |
|
43 '1' |
|
44 >>> process_macros('${{2+2}}') |
|
45 '4' |
|
46 >>> process_macros('${{foo+bar}}',{'foo': 1, 'bar': 2}) |
|
47 '3' |
|
48 |
|
49 Error cases: |
|
50 >>> process_macros('}}') |
|
51 Traceback (most recent call last): |
|
52 ... |
|
53 MacroEvaluationError: Mismatched parens in macro }} |
|
54 >>> process_macros('${{') |
|
55 Traceback (most recent call last): |
|
56 ... |
|
57 MacroEvaluationError: Mismatched parens in macro ${{ |
|
58 |
|
59 Corner cases: |
|
60 >>> process_macros('') |
|
61 '' |
|
62 >>> process_macros(u'') |
|
63 u'' |
|
64 |
|
65 Code execution: |
|
66 >>> process_macros("${{!print 'foo'}}") |
|
67 foo |
|
68 '' |
|
69 >>> process_macros("${{! \\nfor k in range(4):\\n write(str(k))}}") |
|
70 '0123' |
|
71 |
|
72 >>> process_macros('${{if 1\\nfoo\\n$else\\nbar}}') |
|
73 'foo' |
|
74 >>> process_macros('xxx${{if 1\\n${{foo}}\\n$else\\nbar}}yyy',{'foo':42}) |
|
75 'xxx42yyy' |
|
76 >>> process_macros('${{!#}}') |
|
77 '' |
|
78 >>> process_macros('${{"${{foo}}"}}',{'foo':42}) |
|
79 '42' |
|
80 >>> process_macros('${{"${{bar}}"}}',{'foo':42}) |
|
81 Traceback (most recent call last): |
|
82 ... |
|
83 MacroEvaluationError: Error evaluating expression "${{bar}}": NameError: name 'bar' is not defined |
|
84 <BLANKLINE> |
|
85 """ |
|
86 if namespace is None: |
|
87 namespace={} |
|
88 |
|
89 def process_text(text): |
|
90 #print "Processing text: "+repr(text) |
|
91 pos=0 # position of the first character of text that is not yet processed |
|
92 outbuf=[] |
|
93 macrobuf=[] |
|
94 while 1: |
|
95 m=macro_delimiter_re.search(text, pos) |
|
96 if m: |
|
97 ##print "found delimiter: "+text[m.start():] |
|
98 outbuf.append(text[pos:m.start()]) |
|
99 pos=m.start() |
|
100 paren_level=0 |
|
101 for m in macro_delimiter_re.finditer(text,pos): # find a single whole macro expression |
|
102 delim=m.group(1) |
|
103 if delim=='${{': |
|
104 paren_level+=1 |
|
105 elif delim=='}}': |
|
106 paren_level-=1 |
|
107 else: |
|
108 assert 0 |
|
109 if paren_level<0: |
|
110 raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()]) |
|
111 if paren_level == 0: # macro expression finishes here, evaluate it. |
|
112 outbuf.append(process_text(process_macro(text[pos:m.end()]))) |
|
113 pos=m.end() |
|
114 break |
|
115 if paren_level!=0: |
|
116 raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()]) |
|
117 else: # no more macros, just output the rest of the plain text |
|
118 outbuf.append(text[pos:]) |
|
119 break |
|
120 result=''.join(outbuf) |
|
121 #print "Text after processing: "+repr(result) |
|
122 return result |
|
123 def process_macro(macro_expression): |
|
124 #print 'Processing macro: '+repr(macro_expression) |
|
125 try: |
|
126 outstr=StringIO() |
|
127 namespace['write']=outstr.write |
|
128 m=macro_exec_re.match(macro_expression) # ${{!code}} -- exec the code |
|
129 if m: |
|
130 exec m.group(2) in namespace |
|
131 else: |
|
132 m=macro_if_re.match(macro_expression) # ${{if -- if expression |
|
133 if m: |
|
134 outstr.write(handle_if(process_text(m.group(1)),namespace)) |
|
135 else: |
|
136 m=macro_eval_re.match(macro_expression) |
|
137 if m: # ${{code}} -- eval the code |
|
138 outstr.write(str(eval(m.group(1),namespace))) |
|
139 else: |
|
140 raise MacroEvaluationError, 'Invalid macro' |
|
141 #print 'Macro result: '+repr(outstr.getvalue()) |
|
142 return outstr.getvalue() |
|
143 except: |
|
144 raise MacroEvaluationError, 'Error evaluating expression "%s": %s'%( |
|
145 macro_expression, |
|
146 '\n'.join(traceback.format_exception_only(sys.exc_info()[0],sys.exc_info()[1]))) |
|
147 def if_tokenized(code): |
|
148 #print "code: "+repr(code) |
|
149 lines=code.split('\n') |
|
150 yield ('if',lines[0]) |
|
151 #print 'lines: '+repr(lines) |
|
152 for line in lines[1:]: |
|
153 m=re.match('\$(elif|else)(.*)',line) |
|
154 if m: |
|
155 yield (m.group(1),m.group(2)) |
|
156 else: |
|
157 #print "data line "+repr(line) |
|
158 yield ('data',line) |
|
159 raise StopIteration |
|
160 def handle_if(code,namespace): |
|
161 outbuf=[] |
|
162 true_condition_found=0 |
|
163 for token,content in if_tokenized(code): |
|
164 if token=='data' and true_condition_found: |
|
165 outbuf.append(content) |
|
166 else: |
|
167 if true_condition_found: # end of true block reached |
|
168 break |
|
169 if token=='if' or token=='elif': |
|
170 true_condition_found=eval(content,namespace) |
|
171 elif token=='else': |
|
172 true_condition_found=1 |
|
173 return '\n'.join(outbuf) |
|
174 return process_text(instr) |
|
175 |
|
176 def process_file(infilename,namespace): |
|
177 outfilename=outfilename_from_infilename(infilename) |
|
178 infile=open(infilename,'rt') |
|
179 outfile=open(outfilename,'wt') |
|
180 outfile.write(process_macros(infile.read(),namespace)) |
|
181 outfile.close() |
|
182 infile.close() |
|
183 |
|
184 def templatefiles_in_tree(rootdir): |
|
185 files=[] |
|
186 for dirpath, dirnames, filenames in os.walk(rootdir): |
|
187 files+=[os.path.join(dirpath,x) for x in filenames if is_templatefile(x)] |
|
188 return files |
|
189 |
|
190 def misctest(): |
|
191 files=templatefiles_in_tree('.') |
|
192 print "Templates in tree: "+str(files) |
|
193 |
|
194 for k in files: |
|
195 print "Processing file: "+k |
|
196 process_file(k,namespace) |
|
197 # import code |
|
198 # code.interact(None,None,locals()) |
|
199 |
|
200 def _test(): |
|
201 import doctest |
|
202 doctest.testmod() |
|
203 |
|
204 if __name__ == "__main__": |
|
205 _test() |
|
206 |
|
207 |