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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 133 - (show annotations) (download) (as text)
Mon Sep 19 00:26:16 2005 UTC (9 years, 1 month ago) by hadfield
File MIME type: text/x-python
File size: 22868 byte(s)
added an isset function and fixed a bug in the 'len' template function
1 # Copyright 2004-2005 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3 #
4
5 """A template handler/parser module.
6
7 General Info:
8
9 Variables can be set in one of two ways; either the param() or params()
10 method. The param() method takes two arguments, 'key', and 'value', where
11 'key is the name of the template parameter, and value is the value it
12 should be replaced with.
13
14 This module uses caching to speed up template creation. Use the 'use_cache'
15 attribute to toggle caching. Also, remember to clean out the template cache
16 folder every once in a while as you'll probably get some stale cache files.
17
18 Loops:
19
20 To use a loop in your template, you can either do something like:
21 {LOOP MYLOOP}
22 this is loop {MYLOOP}
23 {!LOOP}
24
25 If you have the loop {'MYLOOP': range(1, 10)}, then the above will print out
26 'this is loop X' 10 times. You can also use a more complex loop, say
27 'MYLOOP' is set to:
28 [{'val1': 1.1, 'val2': 1.2}, {'val1': 2.1, 'val2': '2.2'}], then
29 {LOOP MYLOOP}
30 loop values: val1 = {MYLOOP.val1}, val2 = {MYLOOP.val2}
31 {!LOOP}
32
33 will output:
34 loop values: val1 = 1.1, val2 = 1.2
35 loop values: val1 = 2.1, val2 = 2.2
36
37
38 Ifs:
39
40 If statements can be used as follows:
41 {IF TAG == 'value'}
42 {TAG} is equal to 'value'
43 {ELSE}
44 {TAG} is not equal to 'value'
45 {!IF}
46
47 The currently supported operators include:
48 == != > <
49 """
50
51 __revision__ = '$Id$'
52 __authors__ = ["Scott Hadfield <hadfield@gentoo.org>",
53 "Ian Leitch <port001@gentoo.org>"]
54 __modulename__ = 'template'
55
56 import os
57 import re
58
59 ATOM = r'[\(\)\w]'
60 VAR_REGEX = r'%s+(?:\.%s+)*' % (ATOM, ATOM)
61 LITERAL_REGEX = r'"[^"]*"|"?\d+"?'
62 VAR_EXPR = r'(?:%s|%s)' % (VAR_REGEX, LITERAL_REGEX)
63 EXPR_REGEX = r'(%s) (==|!=|<|>) (%s)' % (VAR_EXPR, VAR_EXPR)
64
65 class ParseTree:
66 """The ParseTree object for template parse trees."""
67
68 def __init__(self):
69 pass
70
71 def _find_closer(self, content, opener, closer):
72 """Returns the index of the closing marker for this block of text.
73
74 @param content - The block of text to work with.
75 @param opener - The opening of a block
76 @param closer - The closing of a block.
77 @return - The index at the start position of the closer.
78
79 >>> block = '{O}{O}{O}{C}{O}{O}{C}{C}{C}{C}'
80 >>> ParseTree()._find_closer(block, '{O}', '{C}')
81 27
82 >>> block = '{O}{O}{C}{O}{C}{C}'
83 >>> ParseTree()._find_closer(block, '{O}', '{C}')
84 15
85 """
86
87 end = 0
88 pos = len(opener)
89 open_count = 1
90 while True:
91 start = content.find(opener, pos)
92 end = content.find(closer, pos)
93 if end == -1:
94 raise TemplateError("Could not locate end of block: %s" %
95 content)
96 if end < start or start == -1:
97 open_count -= 1
98 pos = end + len(closer)
99 else:
100 open_count += 1
101 pos = start + len(opener)
102 if open_count == 0:
103 break
104
105 return end
106
107 def _min(self, val1, val2):
108 """Same as builtin min, but doesn't accept None as the min."""
109
110 if val1 is None or val2 is None:
111 return val1 or val2
112 return min(val1, val2)
113
114 def _split_if(self, if_block):
115 """Break an if block into it's pre- and post-else parts.
116
117 >>> block = 'b1{IF X == Y}a1{ELSE}a2{!IF}{ELSE}b2{IF W == Y}' + \
118 'c1{ELSE}c2{!IF}{!IF}'
119 >>> ParseTree()._split_if(block)
120 ('b1{IF X == Y}a1{ELSE}a2{!IF}', 'b2{IF W == Y}c1{ELSE}c2{!IF}')
121 """
122
123 re_if = re.compile(r'(?s)\{IF (%s)\}' % EXPR_REGEX)
124
125 # Mark all nested if blocks
126 if_blocks = []
127 start_if = re_if.search(if_block)
128 if start_if is None:
129 pos = 0
130 else:
131 pos = start_if.start() - 1
132
133 while True:
134 next_if = re_if.search(if_block, pos)
135 if next_if is None:
136 break
137
138 close_index = self._find_closer(if_block[next_if.start():],
139 "{IF ", "{!IF}")
140 pos += close_index + len("{!IF}")
141 if_blocks.append((next_if.start(), pos))
142
143 pos = 0
144 while True:
145 else_index = if_block.find("{ELSE}", pos)
146 if else_index == -1:
147 else_index = len(if_block)
148 break
149 elif self._value_outside_of(else_index, if_blocks):
150 break
151 else:
152 pos += else_index + len("{ELSE}")
153
154 if_block = if_block[:-len("{!IF}")]
155 content_block = if_block[:else_index]
156 else_block = if_block[else_index + len("{ELSE}"):]
157
158 return content_block, else_block
159
160 def _value_outside_of(self, index, val_list):
161 """Returns true if index doesn't fall within any of the val_list pairs.
162
163 >>> ParseTree()._value_outside_of(0, ((1, 3), (7, 70), (75, 80)))
164 True
165 >>> ParseTree()._value_outside_of(1, ((1, 3), (7, 70), (75, 80)))
166 False
167 >>> ParseTree()._value_outside_of(5, ((1, 3), (7, 70), (75, 80)))
168 True
169 >>> ParseTree()._value_outside_of(65, ((0, 3), (7, 70), (75, 80)))
170 False
171 >>> ParseTree()._value_outside_of(70, ((0, 3), (7, 70), (75, 80)))
172 False
173 >>> ParseTree()._value_outside_of(88, ((0, 3), (7, 70), (75, 80)))
174 True
175 """
176 for val_pair in val_list:
177 if index >= val_pair[0] and index <= val_pair[1]:
178 return False
179 return True
180
181 def parse(self, content):
182 """Parses 'content' into a parse tree.
183
184 @param content - Plain text string.
185 @result - A parse tree representing the template.
186 """
187
188 pos = 0
189 parse_tree = {}
190
191 re_var = re.compile(r'\{((?:%s)+)\}' % VAR_REGEX)
192 re_cmd = re.compile(r'(?s)\{(INCLUDE) ([^\}]+)\}')
193 re_loop = re.compile(r'(?s)\{LOOP (%s)\}' % VAR_REGEX)
194 re_if = re.compile(r'(?s)\{IF (%s)\}' % EXPR_REGEX)
195
196 # First things first: strip out any comments.
197 content = re.sub(r'(?s)\{\*.*?\*\}', '', content)
198
199 while pos < len(content):
200 next = None
201 next_var = re_var.search(content, pos)
202 next_cmd = re_cmd.search(content, pos)
203 next_loop = re_loop.search(content, pos)
204 next_if = re_if.search(content, pos)
205
206 if next_var is not None:
207 next = next_var.start()
208 if next_cmd is not None:
209 next = self._min(next, next_cmd.start())
210 if next_loop is not None:
211 next = self._min(next, next_loop.start())
212 if next_if is not None:
213 next = self._min(next, next_if.start())
214
215 if next is not None and next != pos:
216 parse_tree[pos] = content[pos:next]
217 pos = next
218
219 elif next_var is not None and next == next_var.start():
220 parse_tree[pos] = {'VAR': next_var.group(1)}
221 pos = next_var.end()
222
223 elif next_cmd is not None and next == next_cmd.start():
224 parse_tree[pos] = {"CMD":
225 (next_cmd.group(1), next_cmd.group(2))}
226 pos = next_cmd.end()
227
228 elif next_loop is not None and next == next_loop.start():
229 close_index = self._find_closer(content[next:],
230 "{LOOP ", "{!LOOP}")
231 block = content[next + len(next_loop.group()):
232 next + close_index]
233 parse_tree[pos] = {'LOOP':
234 self.parse_loop(next_loop.group(1), block)}
235 pos = next + close_index + len("{!LOOP}")
236
237 elif next_if is not None and next == next_if.start():
238 close_index = self._find_closer(content[next:],
239 "{IF ", "{!IF}")
240 block = content[next + len(next_if.group()):
241 next + close_index + len("{!IF}")]
242 parse_tree[pos] = {'IF':
243 self.parse_if(next_if.group(1), block)}
244 pos = next + close_index + len("{!IF}")
245
246 else:
247 parse_tree[pos] = content[pos:]
248 break
249
250 return parse_tree
251
252 def parse_loop(self, variable, content):
253 """Returns the parse tree for a loop."""
254 return {'VAR': variable,
255 'CONTENT': self.parse(content)}
256
257 def parse_if(self, expression, content):
258 """Returns the parse tree for an if statement."""
259
260 if_content, else_content = self._split_if(content)
261
262 return {'EXPR': expression,
263 'IF_CONTENT': self.parse(if_content),
264 'ELSE_CONTENT': self.parse(else_content)}
265
266 def unparse(self, parse_tree):
267 """Converts a parse tree to it's plain text representation.
268
269 No interpretation. Mostly useful for debugging.
270 """
271
272 result = ""
273 places = parse_tree.keys()
274 places.sort()
275
276 for place in places:
277 if type(parse_tree[place]) is str:
278 result += parse_tree[place]
279
280 elif parse_tree[place].keys()[0] == "VAR":
281 result += '{' + parse_tree[place]["VAR"] + '}'
282
283 elif parse_tree[place].keys()[0] == "CMD":
284 result += '{' + parse_tree[place]["CMD"][0] + \
285 parse_tree[place]["CMD"][1] + '}'
286
287 elif parse_tree[place].keys()[0] == "LOOP":
288 result += '{LOOP ' + parse_tree[place]["LOOP"]["VAR"] + '}'
289 result += self.unparse(parse_tree[place]["LOOP"]["CONTENT"])
290 result += '{!LOOP}'
291
292 elif parse_tree[place].keys()[0] == "IF":
293 result += '{IF ' + parse_tree[place]["IF"]["EXPR"] + '}'
294 result += self.unparse(parse_tree[place]["IF"]["IF_CONTENT"])
295 if parse_tree[place]["IF"]["ELSE_CONTENT"] != {}:
296 result += (
297 '{ELSE}' +
298 self.unparse(parse_tree[place]["IF"]["ELSE_CONTENT"]))
299 result += '{!IF}'
300
301 return result
302
303
304 class Template:
305 """Template handler class. Gets a template parse tree and compiles it."""
306
307 def __init__(self, template = ""):
308
309 self._template = template
310 self._terms = {}
311 self._output = ""
312 self.__compiled = False
313
314 # Where is the cache located?
315 self.cache_dir = "tmpl_cache/"
316
317 # Do you want to use template caching?
318 self.use_cache = False
319
320 def _call_function(self, funcs, value, var):
321 """Returns the result of running 'func' on 'value'."""
322
323 retval = value
324 for func in funcs:
325
326 if func == "len":
327 if retval is None:
328 retval = 0
329 continue
330
331 try:
332 retval = len(retval)
333 except TypeError:
334 retval = len(str(retval))
335
336 if func == "iterator":
337 """Returns the current index in loop iterations."""
338 if var.rfind('.') == -1:
339 retval = None
340 else:
341 retval = var[(var.rfind('.')) + 1:]
342
343 if func == "is_odd":
344 if int(retval) % 2 == 1:
345 retval = 1
346 else:
347 retval = 0
348
349 if func == "isset":
350 if value == "" or value == "{%s}" % var:
351 retval = 0
352 else:
353 retval = 1
354
355 return retval
356
357 def _split_functions(self, var):
358 """Seperate the function calls from the variable they're acting on.
359
360 >>> Template()._split_functions('SIMPLE_VAR')
361 ([], 'SIMPLE_VAR')
362 >>> Template()._split_functions('func1(SIMPLE_LOOP)')
363 (['func1'], 'SIMPLE_LOOP')
364 >>> Template()._split_functions('func1(func2(SIMPLE_LOOP))')
365 (['func2', 'func1'], 'SIMPLE_LOOP')
366 """
367
368 funcs = []
369
370 while var.find('(') != -1:
371 func, var = re.search(r'([^\(]+)\((.+)\)', var).groups()
372 funcs.insert(0, func)
373
374 return funcs, var
375
376 def _get_value(self, var):
377 """Returns the stored parameter matching the description of 'var'.
378
379 >>> tmpl = Template()
380 >>> tmpl._terms = {'test_term': 'j', 'list1': {'list2': 'found me'}}
381 >>> tmpl._get_value('test_term')
382 'j'
383 >>> tmpl._get_value('list1.list2')
384 'found me'
385 """
386
387 # Is this a function call? Break out the actual variable if it is.
388 funcs, var = self._split_functions(var)
389
390 # Convert the template parameter into a python interpretable variable.
391 pyvar = self._term_to_pyvar(var)
392
393 try:
394 # Make sure no evilness can get passed to the eval
395 if re.sub(r'[\w\'"\[\]]', "", pyvar) != "":
396 raise ValueError, "Can't interpret '%s'!" % pyvar
397
398 value = eval("self._terms" + pyvar)
399
400 except KeyError:
401 if not __debug__:
402 value = ""
403 if len(funcs):
404 value = '{%s(%s)}' % (funcs, self._pyvar_to_term(pyvar))
405 value = '{%s}' % self._pyvar_to_term(pyvar)
406
407 if len(funcs):
408 value = self._call_function(funcs, value, var)
409
410 return value
411
412 def _read(self):
413 """Load the content of the template file and return the contents."""
414
415 try:
416 return open(self._template, "r").read()
417 except IOError, errmsg:
418 raise TemplateError("Caught an IOError exception: '%s'" % errmsg)
419
420 def _read_include(self, include):
421 """Returns the contents of the specified INCLUDE."""
422
423 # Path is relative. So use this template's path as the basedir.
424 path = os.path.dirname(self._template) + "/"
425
426 return open(path + include, "r").read()
427
428 def _check_cache(self, digest):
429 """Verify the existence of the cache with digest 'digest'."""
430
431 if not os.path.exists(self.cache_dir):
432 raise TemplateError('Template cache directory missing')
433
434 for filename in os.listdir(self.cache_dir):
435 if filename.endswith('.cache'):
436 if digest == filename.split('.')[0]:
437 return True
438
439 return False
440
441 def _read_cache(self, digest):
442 """Load the cached template and store it in _output."""
443
444 try:
445 readfd = open(
446 os.path.join(self.cache_dir, digest + '.cache'), 'r')
447 readfd.close()
448 return readfd.read()
449
450 except IOError, errmsg:
451 raise TemplateError('Failed to open template cache file',
452 errmsg)
453
454 def _write_cache(self, digest):
455 """Write the cached file to disk."""
456
457 try:
458 writefd = open(
459 os.path.join(self.cache_dir, digest + '.cache'), 'w')
460 writefd.close()
461 writefd.write(self._output)
462
463 except IOError, err:
464 raise TemplateError('Template cache write failed', err)
465
466 def _term_to_pyvar(self, var):
467 """Converts a 'dotted' variable to a python variable.
468
469 >>> Template()._term_to_pyvar('var1')
470 "['var1']"
471 >>> Template()._term_to_pyvar('var1.innervar')
472 "['var1']['innervar']"
473 >>> Template()._term_to_pyvar('var1.inner.very_inner')
474 "['var1']['inner']['very_inner']"
475 >>> Template()._term_to_pyvar('var1.0')
476 "['var1'][0]"
477 """
478
479 pyvar = ""
480 parts = var.split(".")
481 for part in parts:
482 try:
483 inner_val = "[%i]" % int(part)
484 except ValueError:
485 inner_val = "['%s']" % part
486 pyvar += inner_val
487
488 return pyvar
489
490 def _pyvar_to_term(self, var):
491 """Converts a python variable to a 'dotted' variable for template use.
492
493 >>> Template()._pyvar_to_term("['var1']")
494 'var1'
495 >>> Template()._pyvar_to_term("['var1']['innervar']")
496 'var1.innervar'
497 >>> Template()._pyvar_to_term("['var1']['inner']['very_inner']")
498 'var1.inner.very_inner'
499 >>> Template()._pyvar_to_term("['var1'][0]")
500 'var1.0'
501 """
502
503 # Chop off the start and end [, ], and then replace the rest with .'s
504 var = var[1:-1]
505 var = var.replace("][", '.')
506 var = var.replace("'", "")
507 return var
508
509 def _value_of(self, var):
510 """Returns the literal value of any variable or literal."""
511
512 # If it's surrounded by quotes or all digits then we have a literal
513 if ((var[0] == "\"" and var[-1] == "\"") or
514 re.match(r'^\d*$', var) is not None):
515 return var.strip("\"")
516
517 else:
518 return self._get_value(var)
519
520 def evaluate_expression(self, expr):
521 """Evaluate any boolean expressions
522
523 expr should contain an operator (i.e. ==, <, >, !=) and this
524 function will return True or False.
525 """
526
527 match = re.search(EXPR_REGEX, expr)
528 if match is None:
529 raise TemplateError("Error parsing IF expression.")
530
531 lhs = self._value_of(match.group(1))
532 rhs = self._value_of(match.group(3))
533 oper = match.group(2)
534
535 if re.sub(r'[0-9]', "", str(lhs)) == "" and lhs != "":
536 lhs = int(lhs)
537 if re.sub(r'[0-9]', "", str(rhs)) == "" and rhs != "":
538 rhs = int(rhs)
539
540 if oper == "==" and lhs == rhs:
541 return True
542 elif oper == "<" and lhs < rhs:
543 return True
544 elif oper == ">" and lhs > rhs:
545 return True
546 elif oper == "!=" and lhs != rhs:
547 return True
548
549 return False
550
551 def interpret(self, parse_tree):
552 """Interpret the given parse tree.
553
554 This functions returns a fully evaluated template based on the
555 parameters that were set.
556 """
557
558 result = ""
559 places = parse_tree.keys()
560 places.sort()
561
562 for place in places:
563
564 if type(parse_tree[place]) is str:
565 result += parse_tree[place]
566
567 elif parse_tree[place].keys()[0] == "VAR":
568 result += self.interpret_var(parse_tree[place]["VAR"])
569
570 elif parse_tree[place].keys()[0] == "CMD":
571 result += self.interpret_cmd(parse_tree[place]["CMD"])
572
573 elif parse_tree[place].keys()[0] == "LOOP":
574 result += self.interpret_loop(
575 parse_tree[place]["LOOP"]["VAR"],
576 parse_tree[place]["LOOP"]["CONTENT"])
577
578 elif parse_tree[place].keys()[0] == "IF":
579 result += self.interpret_if(
580 parse_tree[place]["IF"]["EXPR"],
581 parse_tree[place]["IF"]["IF_CONTENT"],
582 parse_tree[place]["IF"]["ELSE_CONTENT"])
583
584 return result
585
586 def interpret_var(self, var):
587 """Returns the value if self._terms[var] if it exists."""
588
589 retval = self._get_value(var)
590 return str(retval)
591
592 def interpret_cmd(self, (cmd, parameters)):
593 """Performs a specific action for each possible command.
594
595 Command | Action
596 INCLUDE | Loads the specified include file.
597 """
598
599 if cmd == "INCLUDE":
600 return self.interpret(
601 ParseTree().parse(self._read_include(parameters)))
602 return ""
603
604 def interpret_loop(self, var, content):
605 """Interprets a loop.
606
607 This function works by [probably inefficiently] by first unparsing
608 the content block, replacing all variables with a way to properly
609 reference their actual value, and re-parsing it. For example, if the
610 loop is {THE_LOOP} and it has a value, {THE_LOOP.myvalue}, on the
611 first iteration of the loop, {THE_LOOP.myvalue} will be altered to
612 {THE_LOOP.0.myvalue} so as to reference the first object in
613 THE_LOOP.
614 """
615
616 loop_val = self._get_value(var)
617 loop_content = ""
618 if type(loop_val) in (list, tuple):
619 for i in range(0, len(loop_val)):
620 tmp = ParseTree().unparse(content)
621 tmp = re.sub(r'(\{[^\}]*\b)%s(\b[^\}]*\})' % var,
622 r'\1%s.%i\2' % (var, i), tmp)
623 loop_content += self.interpret(ParseTree().parse(tmp))
624
625 return loop_content
626
627 def interpret_if(self, expr, if_content, else_content):
628 """Interprets the two 'winning' half of the loop."""
629
630 if self.evaluate_expression(expr):
631 return self.interpret(if_content)
632 else:
633 return self.interpret(else_content)
634
635 def compile(self):
636 """Processes the template and save it in our self._output variable."""
637
638 import md5
639 import thread
640
641 if self.use_cache:
642 digest_str = str(self._template) + str(self._terms)
643 digest = md5.new(digest_str).hexdigest()
644
645 if self._check_cache(digest):
646 self._output = self._read_cache(digest)
647
648 else:
649 self._output = self.interpret(ParseTree().parse(self._read()))
650 if self.use_cache:
651 thread.start_new_thread(self._write_cache, (digest,))
652
653 self.__compiled = True
654
655 def output(self):
656 """Returns the resulting template."""
657
658 if not self.__compiled:
659 self.compile()
660
661 return self._output
662
663 def param(self, key, value):
664 """Add the specified parameter to our parameter list.
665
666 This method is here for backwards compatibility only.
667 """
668 self._terms.update({key: value})
669
670 def params(self, terms):
671 """Add a dict of values to our terms."""
672 self._terms.update(terms)
673
674 def reset(self):
675 """Reset this template object."""
676 self.__compiled = False
677 self._output = ""
678 self._terms = {}
679
680 def set_template(self, template):
681 """Allow the user to manually set the template."""
682 self.__compiled = False
683 self._output = ""
684 self._template = template
685
686
687 class TemplateError(Exception):
688 """The base exception for any template errors"""
689 pass

Properties

Name Value
svn:keywords Id

  ViewVC Help
Powered by ViewVC 1.1.20