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 = list(_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)
369 (valref, args) = xxx_todo_changeme
371 parts = _re_subst.split(fmt)
372 for i
in range(len(parts)):
374 if i%2 == 1
and piece !=
'%':
383 (valref, reader) = xxx_todo_changeme1
390 "If any value is a non-empty string or non-empty list, then T else F."
391 (valrefs, t_section, f_section) = args
393 for valref
in valrefs:
397 self.
_do_if(value, t_section, f_section, fp, ctx)
400 ((valref, value), t_section, f_section) = args
401 list, idx = ctx.for_index[valref[0]]
406 elif value ==
'first':
408 elif value ==
'last':
409 value = idx == len(list)-1
411 value = idx == int(value)
412 self.
_do_if(value, t_section, f_section, fp, ctx)
415 ((left_ref, right_ref), t_section, f_section) = args
417 value = string.lower(
_get_value(left_ref, ctx)) == string.lower(value)
418 self.
_do_if(value, t_section, f_section, fp, ctx)
420 def _do_if(self, value, t_section, f_section, fp, ctx):
421 if t_section
is None:
422 t_section = f_section
428 if section
is not None:
432 ((valref,), unused, section) = args
434 if isinstance(list, StringType):
437 ctx.for_index[refname] = idx = [ list, 0 ]
441 del ctx.for_index[refname]
444 "Return a value suitable for [if-any bool_var] usage in a template."
451 """refname -> a string containing a dotted identifier. example:"foo.bar.bang"
452 for_names -> a list of active for sequences.
454 Returns a `value reference', a 3-Tupel made out of (refname, start, rest),
455 for fast access later.
458 if refname[0] ==
'"':
459 return None, refname[1:-1],
None
462 if refname[:3] ==
'arg':
464 idx = int(refname[3:])
468 if idx < len(file_args):
469 return file_args[idx]
471 parts = string.split(refname,
'.')
474 while rest
and (start
in for_names):
476 name = start +
'.' + rest[0]
477 if name
in for_names:
482 return refname, start, rest
485 """(refname, start, rest) -> a prepared `value reference' (see above).
486 ctx -> an execution context instance.
488 Does a name space lookup within the template name space. Active
489 for blocks take precedence over data dictionary members with the
492 (refname, start, rest) = xxx_todo_changeme2
496 if start
in ctx.for_index:
497 list, idx = ctx.for_index[start]
499 elif start
in ctx.data:
507 ob = getattr(ob, attr)
508 except AttributeError:
512 if isinstance(ob, IntType)
or isinstance(ob, FloatType):
522 """A container for the execution context"""
526 "Abstract class which allows EZT to detect Reader objects."
529 """Reads templates from the filesystem."""
531 self.
text = open(fname,
'rb').read()
532 self.
_dir = os.path.dirname(fname)
537 """'Reads' a template from provided text."""
545 """Parent class of all EZT exceptions."""
547 class ArgCountSyntaxError(EZTException):
548 """A bracket directive got the wrong number of arguments."""
551 """The template references an object not contained in the data dictionary."""
554 """The object dereferenced by the template is no sequence (tuple or list)."""
557 """This error may be simply a missing [end]."""
560 """This error may be caused by a misspelled if directive."""
563 """Base location is unavailable, which disables includes."""
568 assert _re_parse.split(
'[a]') == [
'',
'[a]',
None,
'']
569 assert _re_parse.split(
'[a] [b]') == \
570 [
'',
'[a]',
None,
' ',
'[b]',
None,
'']
571 assert _re_parse.split(
'[a c] [b]') == \
572 [
'',
'[a c]',
None,
' ',
'[b]',
None,
'']
573 assert _re_parse.split(
'x [a] y [b] z') == \
574 [
'x ',
'[a]',
None,
' y ',
'[b]',
None,
' z']
575 assert _re_parse.split(
'[a "b" c "d"]') == \
576 [
'',
'[a "b" c "d"]',
None,
'']
577 assert _re_parse.split(
r'["a \"b[foo]" c.d f]') == \
578 [
'',
'["a \\"b[foo]" c.d f]',
None,
'']
582 verbose =
"-v" in argv
583 return doctest.testmod(ezt, verbose=verbose)
585 if __name__ ==
"__main__":
588 sys.exit(
_test(sys.argv)[0])