/[glsr]/trunk/harmonious/lib/template.py
Gentoo

Contents of /trunk/harmonious/lib/template.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 7 - (show annotations) (download) (as text)
Thu Jun 16 00:41:45 2005 UTC (9 years, 6 months ago) by port001
File MIME type: text/x-python
File size: 20698 byte(s)
Convert Header to Id
1 # Copyright 2004-2005 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3 #
4
5 """The template handler module.
6
7 General Info:
8
9 There are two types of values you can pass to the template module, 'terms' and
10 'loops'. Term variables should not be a SequenceType. Loops should be
11 sequences. Loops can also be sequences of dicts to allow for more complex
12 variables within loops. You can set variables one of two ways:
13
14 1. Use the param('PARAM_NAME', term_val, 'term') function - This will replace
15 all occurrences of {PARAM_NAME} in the template with the value of term_val. The
16 last parameter 'term', is used to specifiy if this variable is a term or a
17 loop.
18
19 2. Use the compile('template_path', term_dict, loop_dict) - This will pass a
20 set of terms and loops to the template object. Compile is the last function
21 generally executed before the template is printed.
22
23 This module also uses caching to speed up template creation. You can disable
24 the caching by setting _use_cache = False. Also, remember to clean out the
25 template cache folder as you'll probably get some stale cache files. If you
26 only want a specific template to not be cached then pass cache = False to the
27 compile() function. Note that the compile() cache parameter will only override
28 the _use_cache variable if _cache_override is set to True.
29
30
31 Loops:
32
33 To use a loop in your template, you can either do something like:
34 {LOOP MYLOOP}
35 this is loop {MYLOOP}
36 {!LOOP}
37
38 If you have the loop {'MYLOOP': range(1, 10)}, then the above will print out
39 'this is loop X' 10 times. You can also use a more complex loop, say 'MYLOOP'
40 is set to [{'val1': 1.1, 'val2': 1.2}, {'val1': 2.1, 'val2': '2.2'}], then
41 {LOOP MYLOOP}
42 loop values: val1 = {MYLOOP.val1}, val2 = {MYLOOP.val2}
43 {!LOOP}
44
45 will output:
46 loop values: val1 = 1.1, val2 = 1.2
47 loop values: val1 = 2.1, val2 = 2.2
48
49
50 Ifs:
51
52 If statements can be used as follows:
53 {IF TAG == 'value'}
54 {TAG} is equal to 'value'
55 {!IF}
56
57 The currently supported operators include:
58 == != > <
59
60 """
61
62 __revision__ = '$Id$'
63 __authors__ = ["Scott Hadfield <hadfield@gentoo.org>",
64 "Ian Leitch <port001@gentoo.org>"]
65 __modulename__ = 'template'
66
67 import md5
68 import os
69 import re
70 import string
71 import thread
72
73 class Template:
74 """Template handler class."""
75
76 def __init__(self, template_cache = "tmpl_cache/"):
77
78 self._template_loc = ""
79 self._contents = []
80 self._output = []
81 self._loops = {}
82 self._terms = {}
83
84 # Where is the cache located?
85 self._template_cache = template_cache
86
87 # Do you want to use template caching?
88 self._use_cache = False
89
90 # Do you want the compile() cache parameter to override _use_cache?
91 self._cache_override = True
92
93 # These are the template languages keywords. You can't have a parameter
94 # with the same name as a keyword.
95 self._keywords = ("ELSE", "INCLUDE")
96
97 # The standard regex to match template variables. This should match
98 # all alphanumeric strings with potentially embedded .'s (dot's).
99 self._var_regex = r'\w+(?:\.\w+)?\w*'
100
101 # The literal regex to match strings and digits
102 self._literal_regex = r'"[^"]*"|"?\d+"?'
103
104 def _read(self):
105 """Load the content of the template file into self.contents."""
106
107 try:
108 self._contents = open(self._template_loc, "r").readlines()
109
110 except IOError, errmsg:
111 raise TemplateError("Caught an IOError exception: '%s'" % errmsg)
112
113 def _read_include(self, include = ""):
114
115 import os.path
116
117 assert(len(include) > 0)
118
119 if include[0] == "/":
120 path = ""
121 else:
122 # Path is relative. So use this templates path as the basedir.
123 path = os.path.dirname(self._template_loc) + "/"
124
125 contents = open(path + include, "r").read()
126 return contents
127
128 def _sub(self, variable, repl_text, values, index = -1):
129 """Substitute all occurences of 'variable' in repl_text with the value.
130
131 The value is obtained by evaluating 'variable' against 'values'. Values
132 might be self._terms or self._loops. 'index' is used during loops to
133 define what loop iteration we're on and return the appropriate array
134 value.
135 """
136
137 dot_loc = variable.find(".")
138 if dot_loc != -1 and values.has_key(variable[:dot_loc]):
139
140 param_name = variable[:dot_loc]
141 # This will build a "python compatible" list to access the dict
142 # i.e. ['var1']['var2']
143 rest = "['%s']" % variable[dot_loc + 1:].replace(".", "']['")
144
145 if index != -1:
146 rest = "[%s]" % index + rest
147
148 try:
149 value = eval("values[param_name]%s" % rest)
150 except KeyError, errmsg:
151 raise TemplateError('Caught a KeyError exception: ' +
152 "%s not a member of '%s'" %
153 (errmsg, param_name))
154 except IndexError, errmsg:
155 raise TemplateError('Caught an IndexError exception: ' +
156 '%s' % (errmsg))
157
158 # Convert value to a string and then do the replace.
159 repl_text = repl_text.replace("{%s}" % variable, "%s" % value)
160
161 elif values.has_key(variable):
162
163 if index == -1:
164 repl_text = repl_text.replace("{%s}" % variable,
165 "%s" % values[variable])
166 else:
167 repl_text = repl_text.replace("{%s}" % variable,
168 "%s" % values[variable][index])
169
170 return repl_text
171
172 def clear(self):
173 """Re-initializes all of the template variables."""
174
175 self._template_loc = ""
176 self._contents = []
177 self._output = []
178
179 def compile(self, template_loc, terms = None, loops = None, cache = None):
180 """Processes the template.
181
182 This includes 4 steps, stripping out the comments, replacing all
183 simple variables (i.e. {VAR} or {VAR.sub1}), evaluating all LOOPs,
184 and evaluating all IFs.
185 """
186
187 if self._cache_override == True and cache is not None:
188 self._use_cache = cache
189
190 digest_str = str(template_loc) + str(terms) + str(loops)
191 digest = md5.new(digest_str).hexdigest()
192
193 if self._use_cache and self._check_cache(digest):
194 self._read_cache(digest)
195
196 else:
197 self._template_loc = template_loc
198 self._read()
199
200 if terms is not None:
201 for name, term in terms.iteritems():
202 self.param(name, term, "term")
203
204 if loops is not None:
205 for name, loop in loops.iteritems():
206 self.param(name, loop, "loop")
207
208 full_file = "".join(self._contents);
209
210 # Load any INCLUDES
211 while re.search(r'\{INCLUDE ([^\}]+)\}', full_file) is not None:
212 includes = re.findall(r'\{INCLUDE ([^\}]+)\}', full_file)
213 for include in includes:
214 include_content = self._read_include(include)
215 full_file = full_file.replace("{INCLUDE %s}" % include,
216 include_content)
217
218 # Strip out all comments
219 full_file = re.sub(r'(?s)\{\*.*?\*\}', '', full_file)
220
221 # Process all variable tags (i.e. non LOOP, non IF tags)
222 full_file = self._evaluate_vars(full_file)
223
224 # Evaluate all LOOPs and their nested IFs
225 full_file = self._evaluate_loops(full_file)
226
227 # Evaluate the rest of the IFs that weren't in any loops
228 full_file = self._evaluate_ifs(full_file)
229
230 # Build the output variable
231 self._output = string.split(full_file, "\n")
232
233 if self._use_cache:
234 # Write cache file
235 thread.start_new_thread(self._write_cache, (digest,))
236
237 def _add_tmpl_values(self, loop):
238 """Adds values to the loop to make loops easier to use.
239
240 Each loop automatically gets an '_is_odd', '_is_even', '_counter'
241 variable. If that variable is already set it will be overridden.
242 This only applies to multi-dimentional loops.
243 """
244
245 from types import DictType
246
247 for i in range(0, len(loop)):
248 if type(loop[i]) is DictType:
249 loop[i]['_counter'] = i
250 loop[i]['_is_odd'] = i % 2
251 loop[i]['_is_even'] = int(not i % 2)
252
253 return loop
254
255 def _evaluate_vars(self, content):
256 """Evaluate all variable tags."""
257
258 # This regex will find all parameters that are of the form:
259 # {VAR} or {VAR.var} or {VAR.var1.var2} or ...
260 variables = re.findall(r'\{((?:%s)+)\}' % self._var_regex, content)
261 variables = filter(lambda x: x not in self._keywords, variables)
262
263 for variable in variables:
264 content = self._sub(variable, content, self._terms)
265
266 return content
267
268 def _evaluate_loops(self, content):
269 """Evaluate all LOOPs."""
270
271 # This regex will return pairs of ('LOOP_NAME', 'LOOP_CLOSE')
272 # However, each pair will only contain either the LOOP_NAME or
273 # LOOP_CLOSE because of the '|' in the re. This way we know how many
274 # nested loops there will be before the valid loop close.
275 # i.e. [('LOOP_NAME1', ''), ('LOOP_NAME2', ''), ('', 'LOOP'),
276 # ('', 'LOOP')]
277 # tells use we have a loop that looks something like the following:
278 # {LOOP LOOP_NAME1} {LOOP LOOP_NAME2} {!LOOP} {!LOOP}
279
280 loops = re.findall(r'(?s)\{LOOP (%s)\}|\{!(LOOP)\}' % self._var_regex,
281 content)
282
283 # This marks a closed loop
284 loop_close = ('', 'LOOP')
285
286 loop_array = self._build_nest_count(content, loops, loop_close, "LOOP")
287
288 # Note that every time a loop is evaluated, all other loops' starting
289 # positions need to be re-evaluated.
290 for i in range(0, len(loop_array)):
291
292 loop = loop_array[i]
293 content_start_len = len(content)
294
295 start_pos = loop["char_pos"]
296 end_pos = content.find("{!LOOP}", start_pos)
297 if end_pos == -1:
298 raise TemplateError("Unable to find end of loop '%s'." % loop)
299
300 loop_text = content[start_pos + len("{LOOP %s}" % loop["expr"]):
301 end_pos]
302
303 # Cut out the loop text from the full file text
304 content = content[:start_pos] + content[end_pos + 7:]
305
306 dot_loc = loop["expr"].find(".")
307 if dot_loc == -1 and self._loops.has_key(loop["expr"]):
308 var_name = loop["expr"]
309 else:
310 raise TemplateError("Loop '%s' is not defined." % loop["expr"])
311
312 evaluated_text = ""
313 for j in range(0, len(self._loops[var_name])):
314
315 variables = re.findall(r'\{((?:%s)+)\}' % self._var_regex,
316 loop_text)
317 variables = filter(lambda x: x not in self._keywords,
318 variables)
319 # Remove all loop variables that aren't relating to this loop.
320 variables = filter(
321 lambda x: re.search(r'^%s\b' % var_name, x),
322 variables)
323
324 repl_text = loop_text
325
326 for variable in variables:
327 repl_text = self._sub(variable, repl_text, self._loops, j)
328
329 # We also have to evaluate all if tags within the loop at
330 # this point
331 repl_text = self._evaluate_ifs(repl_text, j)
332
333 evaluated_text += repl_text
334
335 # Insert the evaluated text
336 content = (content[:start_pos] + evaluated_text +
337 content[start_pos:])
338
339 loop_array = self._calibrate(content, content_start_len,
340 loop_array, i)
341
342 return content
343
344 def _evaluate_ifs(self, content, index = -1):
345 """Evaluate all IFs."""
346
347 var_expr = r'(?:%s|%s)' % (self._var_regex, self._literal_regex)
348 expr_regex = r'%s (?:==|!=|<|>) %s' % (var_expr, var_expr)
349
350 # See _evaluate_loops for a comment on how this expression works.
351 ifs = re.findall(r'(?s)\{IF (%s)\}|\{!(IF)\}' % expr_regex, content)
352 if_close = ('', 'IF')
353
354 if_array = self._build_nest_count(content, ifs, if_close, "IF")
355
356 # Now do the evaluation.
357 for i in range(0, len(if_array)):
358
359 cur_if = if_array[i]
360 content_start_len = len(content)
361
362 start_pos = cur_if["char_pos"]
363 end_pos = content.find("{!IF}", start_pos)
364 if end_pos == -1:
365 raise TemplateError("Unable to find end of loop '%s'." %
366 cur_if)
367
368 if_text = content[start_pos + len("{IF %s}" % cur_if["expr"]):
369 end_pos]
370 else_text = ""
371
372 else_start_pos = if_text.find("{ELSE}")
373 if else_start_pos != -1:
374 else_text = if_text[else_start_pos + len("{ELSE}"):]
375 if_text = if_text[:else_start_pos]
376
377 # Evaluate the expression:
378 if self._evaluate_expr(cur_if["expr"], index):
379 new_text = if_text
380 else:
381 new_text = else_text
382
383 content = (content[:start_pos] + new_text +
384 content[end_pos + len("{!IF}"):])
385
386 if_array = self._calibrate(content, content_start_len, if_array, i)
387
388 return content
389
390 def _calibrate(self, content, content_start_len, region_array, index):
391 """Calibrate the starting character positions for a set of regions.
392
393 This is required because once an IF or LOOP is processed the number
394 of characters for the output changes meaning that our region starting
395 positions will all be offset.
396 """
397
398 difference = len(content) - content_start_len
399 for j in range(index, len(region_array)):
400 if region_array[j]["char_pos"] > region_array[index]["char_pos"]:
401 region_array[j]["char_pos"] += difference
402
403 return region_array
404
405 def _build_nest_count(self, content, regions, region_close, region_str):
406 """Generates an array of (region, nest) pairs.
407
408 Nest is a count of how many nested loops a region has and each
409 region is basically either LOOP, or IF starting position up to 'close'.
410 This data is used for the evaluation order of regions. For example,
411 the least nested loops will always be evaluated first. This eliminates
412 the problem of nested loops by evaluating the inner loops first.
413 The same goes for IF's.
414 """
415
416 def region_sort(reg1, reg2):
417 """Sorts loops in order of how nested they are."""
418 if reg1["nested"] < reg2["nested"]:
419 return -1
420 if reg1["nested"] > reg2["nested"]:
421 return 0
422 if reg1["char_pos"] < reg2["char_pos"]:
423 return -1
424 if reg1["char_pos"] > reg2["char_pos"]:
425 return 0
426 return 1
427
428 # Determine which regions have nested regions, and how many nested
429 # regions we have. This will be used to determine processing order.
430 start_pos = 0 # Used for marking the character position of the region.
431 region_array = []
432 for i in range(0, len(regions)):
433
434 # Skip this iteration if it's a region close
435 if regions[i] == region_close:
436 continue
437
438 # Figure out how many sub regions are contained within this region.
439 region_array.append({"expr": regions[i][0], "nested": 0})
440
441 match_str = "{%s %s}" % (region_str, regions[i][0])
442
443 # Now we want to find the starting position for each region.
444 # Set the char_pos of the current region. Note that we must use -1
445 # here as 'i' isn't valuable due to the skipping of region closes.
446 region_array[-1]["char_pos"] = content.find(match_str, start_pos)
447
448 # Increment past the {} (i.e. past the {LOOP VARNAME} text)
449 start_pos = region_array[-1]["char_pos"] + len(match_str)
450
451 nest_count = 1
452 for j in range(i + 1, len(regions)):
453
454 if regions[j] != region_close:
455 region_array[-1]["nested"] += 1
456 nest_count += 1
457
458 else:
459 nest_count -= 1
460
461 if nest_count == 0:
462 break
463
464 region_array.sort(region_sort)
465 return region_array
466
467 def _evaluate_expr(self, expr, index):
468 """Evaluate any boolean expressions.
469
470 expr should contain an operator (i.e. ==, <, >, !=) and this function
471 will return True or False.
472 """
473
474 variables = r'(?:%s|%s)' % (self._var_regex, self._literal_regex)
475 expr_regex = r'(%s) (==|!=|<|>) (%s)' % (variables, variables)
476
477 match = re.search(expr_regex, expr)
478
479 if match is None:
480 raise TemplateError("Error parsing IF expression.")
481
482 lhs = self._value_of(match.group(1), index)
483 rhs = self._value_of(match.group(3), index)
484 oper = match.group(2)
485
486 # Convert all \n's.
487 return eval("\"%s\" %s \"%s\"" % (str(lhs).replace("\n", r'\n'), oper,
488 str(rhs).replace("\n", r'\n')))
489
490 def _value_of(self, var, index):
491 """Returns the literal value of any variable or literal."""
492
493 # If it's surrounded by quotes or all digits then we have a literal
494 if (var[0] == "\"" and var[-1] == "\"") or \
495 re.match(r'^\d*$', var) is not None:
496 value = var.strip("\"")
497
498 else:
499 value = self._sub(var, "{%s}" % var, self._terms)
500 if value == "{%s}" % var:
501 value = self._sub(var, "{%s}" % var, self._loops, index)
502
503 return value
504
505 def _check_cache(self, digest):
506 """Verify the existence of the cache with digest 'digest'."""
507
508 if not os.path.exists(self._template_cache):
509 raise TemplateError('Template cache directory missing')
510
511 for filename in os.listdir(self._template_cache):
512 if filename.endswith('.cache'):
513 if digest == filename.split('.')[0]:
514 return True
515
516 return False
517
518 def _read_cache(self, digest):
519 """Load the cached template and store it in _output."""
520
521 try:
522 readfd = open(
523 os.path.join(self._template_cache, digest + '.cache'), 'r')
524 self._output = readfd.readlines()
525 readfd.close()
526
527 except IOError, errmsg:
528 raise TemplateError('Failed to open template cache file',
529 errmsg)
530
531 def _write_cache(self, digest):
532 """Write the cached file to disk."""
533
534 try:
535 writefd = open(
536 os.path.join(self._template_cache, digest + '.cache'), 'w')
537 writefd.write('\n'.join(self._output))
538 writefd.close()
539
540 except IOError, err:
541 #logwrite("Cache write failed: '%s'" % err, __modulename__,
542 # 'Error')
543 raise TemplateError('Template cache write failed', err)
544
545 def param(self, key, value, param_type = "term"):
546 """Add a new parameter, either a 'loop' or a 'term'."""
547
548 if string.lower(param_type) in ("l", "loop"):
549 self._loops.update({key: self._add_tmpl_values(value)})
550
551 elif string.lower(param_type) in ("t", "term"):
552 self._terms.update({key: value})
553
554 def output(self):
555 """Returns the resulting template."""
556
557 if len(self._output) == 0:
558 raise TemplateError('No compiled template data found')
559
560 return "\n".join(self._output)
561
562
563 # FIXME: This exception is only here until exception handling is properly setup
564 class TemplateError(Exception): pass

Properties

Name Value
svn:keywords Id

  ViewVC Help
Powered by ViewVC 1.1.20