2 """ezt.py -- easy templating 4 ezt templates are very similar to standard HTML files. But additionally 5 they contain directives sprinkled in between. With these directives 6 it is possible to generate the dynamic content from the ezt templates. 8 These directives are enclosed in square brackets. If you are a 9 C-programmer, you might be familar with the #ifdef directives of the 10 C preprocessor 'cpp'. ezt provides a similar concept for HTML. Additionally 11 EZT has a 'for' directive, which allows to iterate (repeat) certain 12 subsections of the template according to sequence of data items 13 provided by the application. 15 The HTML rendering is performed by the method generate() of the Template 16 class. Building template instances can either be done using external 17 EZT files (convention: use the suffix .ezt for such files): 19 >>> template = Template("../templates/log.ezt") 21 or by calling the parse() method of a template instance directly with 22 a EZT template string: 24 >>> template = Template() 25 >>> template.parse('''<html><head> 26 ... <title>[title_string]</title></head> 27 ... <body><h1>[title_string]</h1> 28 ... [for a_sequence] <p>[a_sequence]</p> 30 ... The [person] is [if-any state]in[else]out[end]. 35 The application should build a dictionary 'data' and pass it together 36 with the output fileobject to the templates generate method: 38 >>> data = {'title_string' : "A Dummy Page", 39 ... 'a_sequence' : ['list item 1', 'list item 2', 'another element'], 40 ... 'person': "doctor", 43 >>> template.generate(sys.stdout, data) 45 <title>A Dummy Page</title></head> 46 <body><h1>A Dummy Page</h1> 49 <p>another element</p> 55 Template syntax error reporting should be improved. Currently it is 56 very sparse (template line numbers would be nice): 58 >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]") 59 Traceback (innermost last): 60 File "<stdin>", line 1, in ? 61 File "ezt.py", line 220, in parse 62 self.program = self._parse(text) 63 File "ezt.py", line 275, in _parse 64 raise ArgCountSyntaxError(str(args[1:])) 65 ArgCountSyntaxError: ['unexpected', 'args'] 66 >>> Template().parse("[if unmatched_end]foo[end]") 67 Traceback (innermost last): 68 File "<stdin>", line 1, in ? 69 File "ezt.py", line 206, in parse 70 self.program = self._parse(text) 71 File "ezt.py", line 266, in _parse 72 raise UnmatchedEndError() 79 Several directives allow the use of dotted qualified names refering to objects 80 or attributes of objects contained in the data dictionary given to the 88 This directive is simply replaced by the value of identifier from the data 89 dictionary. QUAL_NAME might be a dotted qualified name refering to some 90 instance attribute of objects contained in the dats dictionary. 91 Numbers are converted to string though. 93 [include "filename"] or [include QUAL_NAME] 95 This directive is replaced by content of the named include file. 100 [for QUAL_NAME] ... [end] 102 The text within the [for ...] directive and the corresponding [end] 103 is repeated for each element in the sequence referred to by the qualified 104 name in the for directive. Within the for block this identifiers now 105 refers to the actual item indexed by this loop iteration. 107 [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end] 109 Test if any QUAL_NAME value is not None or an empty string or list. 110 The [else] clause is optional. CAUTION: Numeric values are converted to 111 string, so if QUAL_NAME refers to a numeric value 0, the then-clause is 114 [if-index INDEX_FROM_FOR odd] ... [else] ... [end] 115 [if-index INDEX_FROM_FOR even] ... [else] ... [end] 116 [if-index INDEX_FROM_FOR first] ... [else] ... [end] 117 [if-index INDEX_FROM_FOR last] ... [else] ... [end] 118 [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end] 120 These five directives work similar to [if-any], but are only useful 121 within a [for ...]-block (see above). The odd/even directives are 122 for example useful to choose different background colors for adjacent rows 123 in a table. Similar the first/last directives might be used to 124 remove certain parts (for example "Diff to previous" doesn't make sense, 125 if there is no previous). 127 [is QUAL_NAME STRING] ... [else] ... [end] 128 [is QUAL_NAME QUAL_NAME] ... [else] ... [end] 130 The [is ...] directive is similar to the other conditional directives 131 above. But it allows to compare two value references or a value reference 132 with some constant string. 170 from types
import StringType, IntType, FloatType
188 _item =
r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)' 189 _re_parse = re.compile(
r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item))
191 _re_args = re.compile(
r'"(?:[^\\"]|\\.)*"|[-\w.]+')
194 _block_cmd_specs = {
'if-index':2,
'for':1,
'is':2 }
195 _block_cmds = _block_cmd_specs.keys()
200 _re_newline = re.compile(
'[ \t\r\f\v]*\n\\s*')
201 _re_whitespace = re.compile(
r'\s\s+')
207 _re_subst = re.compile(
'%(%|[0-9]+)')
211 def __init__(self, fname=None, compress_whitespace=1):
217 "fname -> a string object with pathname of file containg an EZT template." 222 """Parse the template specified by text_or_reader. 224 The argument should be a string containing the template, or it should 225 specify a subclass of ezt.Reader which can read templates. 227 if not isinstance(text_or_reader, Reader):
238 def _parse(self, reader, for_names=None, file_args=()):
239 """text -> string object containing the HTML template. 241 This is a private helper function doing the real work for method parse. 242 It returns the parsed template as a 'program'. This program is a sequence 243 made out of strings or (function, argument) 2-tuples. 245 Note: comment directives [# ...] are automatically dropped by _re_parse. 249 parts = _re_parse.split(reader.text)
256 for i
in range(len(parts)):
263 piece = _re_whitespace.sub(
' ', _re_newline.sub(
'\n', piece))
264 program.append(piece)
271 args = _re_args.findall(piece)
278 true_section = program[idx:]
280 stack[-1][3] = true_section
286 cmd, idx, args, true_section = stack.pop()
289 else_section = program[idx:]
290 func = getattr(self,
'_cmd_' + re.sub(
'-',
'_', cmd))
291 program[idx:] = [ (func, (args, true_section, else_section)) ]
294 elif cmd
in _block_cmds:
295 if len(args) > _block_cmd_specs[cmd] + 1:
304 for_names.append(args[1][0])
307 stack.append([cmd, len(program), args[1:],
None])
308 elif cmd ==
'include':
309 if args[1][0] ==
'"':
310 include_filename = args[1][1:-1]
314 program.extend(self.
_parse(reader.read_other(include_filename),
323 elif cmd ==
'if-any':
327 stack.append([
'if-any', len(program), f_args,
None])
334 program.append((self.
_cmd_format, (f_args[0], f_args[1:])))
345 """This private helper function takes a 'program' sequence as created 346 by the method '_parse' and executes it step by step. strings are written 347 to the file object 'fp' and functions are called. 350 if isinstance(step, StringType):
353 step[0](step[1], fp, ctx)
359 if hasattr(value,
'read'):
361 chunk = value.read(16384)
370 parts = _re_subst.split(fmt)
371 for i
in range(len(parts)):
373 if i%2 == 1
and piece !=
'%':
388 "If any value is a non-empty string or non-empty list, then T else F." 389 (valrefs, t_section, f_section) = args
391 for valref
in valrefs:
395 self.
_do_if(value, t_section, f_section, fp, ctx)
398 ((valref, value), t_section, f_section) = args
399 list, idx = ctx.for_index[valref[0]]
404 elif value ==
'first':
406 elif value ==
'last':
407 value = idx == len(list)-1
409 value = idx == int(value)
410 self.
_do_if(value, t_section, f_section, fp, ctx)
413 ((left_ref, right_ref), t_section, f_section) = args
415 value = string.lower(
_get_value(left_ref, ctx)) == string.lower(value)
416 self.
_do_if(value, t_section, f_section, fp, ctx)
418 def _do_if(self, value, t_section, f_section, fp, ctx):
419 if t_section
is None:
420 t_section = f_section
426 if section
is not None:
430 ((valref,), unused, section) = args
432 if isinstance(list, StringType):
435 ctx.for_index[refname] = idx = [ list, 0 ]
439 del ctx.for_index[refname]
442 "Return a value suitable for [if-any bool_var] usage in a template." 449 """refname -> a string containing a dotted identifier. example:"foo.bar.bang" 450 for_names -> a list of active for sequences. 452 Returns a `value reference', a 3-Tupel made out of (refname, start, rest), 453 for fast access later. 456 if refname[0] ==
'"':
457 return None, refname[1:-1],
None 460 if refname[:3] ==
'arg':
462 idx = int(refname[3:])
466 if idx < len(file_args):
467 return file_args[idx]
469 parts = string.split(refname,
'.')
472 while rest
and (start
in for_names):
474 name = start +
'.' + rest[0]
475 if name
in for_names:
480 return refname, start, rest
483 """(refname, start, rest) -> a prepared `value reference' (see above). 484 ctx -> an execution context instance. 486 Does a name space lookup within the template name space. Active 487 for blocks take precedence over data dictionary members with the 493 if ctx.for_index.has_key(start):
494 list, idx = ctx.for_index[start]
496 elif ctx.data.has_key(start):
504 ob = getattr(ob, attr)
505 except AttributeError:
509 if isinstance(ob, IntType)
or isinstance(ob, FloatType):
519 """A container for the execution context""" 523 "Abstract class which allows EZT to detect Reader objects." 526 """Reads templates from the filesystem.""" 528 self.
text = open(fname,
'rb').read()
529 self.
_dir = os.path.dirname(fname)
534 """'Reads' a template from provided text.""" 542 """Parent class of all EZT exceptions.""" 544 class ArgCountSyntaxError(EZTException):
545 """A bracket directive got the wrong number of arguments.""" 548 """The template references an object not contained in the data dictionary.""" 551 """The object dereferenced by the template is no sequence (tuple or list).""" 554 """This error may be simply a missing [end].""" 557 """This error may be caused by a misspelled if directive.""" 560 """Base location is unavailable, which disables includes.""" 565 assert _re_parse.split(
'[a]') == [
'',
'[a]',
None,
'']
566 assert _re_parse.split(
'[a] [b]') == \
567 [
'',
'[a]',
None,
' ',
'[b]',
None,
'']
568 assert _re_parse.split(
'[a c] [b]') == \
569 [
'',
'[a c]',
None,
' ',
'[b]',
None,
'']
570 assert _re_parse.split(
'x [a] y [b] z') == \
571 [
'x ',
'[a]',
None,
' y ',
'[b]',
None,
' z']
572 assert _re_parse.split(
'[a "b" c "d"]') == \
573 [
'',
'[a "b" c "d"]',
None,
'']
574 assert _re_parse.split(
r'["a \"b[foo]" c.d f]') == \
575 [
'',
'["a \\"b[foo]" c.d f]',
None,
'']
579 verbose =
"-v" in argv
580 return doctest.testmod(ezt, verbose=verbose)
582 if __name__ ==
"__main__":
585 sys.exit(
_test(sys.argv)[0])
def _do_if(self, value, t_section, f_section, fp, ctx)
def __init__(self, fname=None, compress_whitespace=1)
def _cmd_if_index(self, args, fp, ctx)
def _execute(self, program, fp, ctx)
def parse(self, text_or_reader)
def _parse(self, reader, for_names=None, file_args=())
def generate(self, fp, data)
def _cmd_print(self, valref, fp, ctx)
def _cmd_include(self, valref, reader, fp, ctx)
def _cmd_if_any(self, args, fp, ctx)
def __init__(self, fname)
def _cmd_is(self, args, fp, ctx)
def read_other(self, relative)
def _get_value(refname, start, rest, ctx)
def parse_file(self, fname)
def read_other(self, relative)
def _cmd_for(self, args, fp, ctx)
def _cmd_format(self, valref, args, fp, ctx)
def _prepare_ref(refname, for_names, file_args)