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.
136 # Copyright (C) 2001-2002 Greg Stein. All Rights Reserved.
138 # Redistribution and use in source and binary forms, with or without
139 # modification, are permitted provided that the following conditions are
142 # * Redistributions of source code must retain the above copyright
143 # notice, this list of conditions and the following disclaimer.
145 # * Redistributions in binary form must reproduce the above copyright
146 # notice, this list of conditions and the following disclaimer in the
147 # documentation and/or other materials provided with the distribution.
149 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
150 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
151 # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
152 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
153 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
154 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
155 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
156 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
157 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
158 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
159 # POSSIBILITY OF SUCH DAMAGE.
162 # This software is maintained by Greg and is available at:
163 # http://viewcvs.sourceforge.net/
164 # it is also used by the following projects:
165 # http://edna.sourceforge.net/
170 from types
import StringType
, IntType
, FloatType
174 # This regular expression matches three alternatives:
175 # expr: DIRECTIVE | BRACKET | COMMENT
176 # DIRECTIVE: '[' ITEM (whitespace ITEM)* ']
177 # ITEM: STRING | NAME
178 # STRING: '"' (not-slash-or-dquote | '\' anychar)* '"'
179 # NAME: (alphanum | '_' | '-' | '.')+
181 # COMMENT: '[#' not-rbracket* ']'
183 # When used with the split() method, the return value will be composed of
184 # non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since
185 # the COMMENT matches are not placed into a group, they are considered a
186 # "splitting" value and simply dropped.
188 _item
= r
'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)'
189 _re_parse
= re
.compile(r
'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item
, _item
))
191 _re_args
= re
.compile(r
'"(?:[^\\"]|\\.)*"|[-\w.]+')
193 # block commands and their argument counts
194 _block_cmd_specs
= { 'if-index':2, 'for':1, 'is':2 }
195 _block_cmds
= _block_cmd_specs
.keys()
197 # two regular expressions for compressing whitespace. the first is used to
198 # compress any whitespace including a newline into a single newline. the
199 # second regex is used to compress runs of whitespace into a single space.
200 _re_newline
= re
.compile('[ \t\r\f\v]*\n\\s*')
201 _re_whitespace
= re
.compile(r
'\s\s+')
203 # this regex is used to substitute arguments into a value. we split the value,
204 # replace the relevant pieces, and then put it all back together. splitting
205 # will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or
207 _re_subst
= re
.compile('%(%|[0-9]+)')
211 def __init__(self
, fname
=None, compress_whitespace
=1):
212 self
.compress_whitespace
= compress_whitespace
214 self
.parse_file(fname
)
216 def parse_file(self
, fname
):
217 "fname -> a string object with pathname of file containg an EZT template."
219 self
.program
= self
._parse
(_FileReader(fname
))
221 def parse(self
, text_or_reader
):
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
):
228 # assume the argument is a plain text string
229 text_or_reader
= _TextReader(text_or_reader
)
230 self
.program
= self
._parse
(text_or_reader
)
232 def generate(self
, fp
, data
):
236 self
._execute
(self
.program
, fp
, ctx
)
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.
248 # parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT
249 parts
= _re_parse
.split(reader
.text
)
256 for i
in range(len(parts
)):
258 which
= i
% 3 # discriminate between: TEXT DIRECTIVE BRACKET
260 # TEXT. append if non-empty.
262 if self
.compress_whitespace
:
263 piece
= _re_whitespace
.sub(' ', _re_newline
.sub('\n', piece
))
264 program
.append(piece
)
266 # BRACKET directive. append '[' if present.
270 # DIRECTIVE is present.
271 args
= _re_args
.findall(piece
)
275 raise ArgCountSyntaxError(str(args
[1:]))
276 ### check: don't allow for 'for' cmd
278 true_section
= program
[idx
:]
280 stack
[-1][3] = true_section
283 raise ArgCountSyntaxError(str(args
[1:]))
284 # note: true-section may be None
286 cmd
, idx
, args
, true_section
= stack
.pop()
288 raise UnmatchedEndError()
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:
296 raise ArgCountSyntaxError(str(args
[1:]))
297 ### this assumes arg1 is always a ref
298 args
[1] = _prepare_ref(args
[1], for_names
, file_args
)
300 # handle arg2 for the 'is' command
302 args
[2] = _prepare_ref(args
[2], for_names
, file_args
)
304 for_names
.append(args
[1][0])
306 # remember the cmd, current pos, args, and a section placeholder
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]
313 f_args
.append(_prepare_ref(arg
, for_names
, file_args
))
314 program
.extend(self
._parse
(reader
.read_other(include_filename
),
319 raise ArgCountSyntaxError(str(args
))
320 program
.append((self
._cmd
_include
,
321 (_prepare_ref(args
[1], for_names
, file_args
),
323 elif cmd
== 'if-any':
326 f_args
.append(_prepare_ref(arg
, for_names
, file_args
))
327 stack
.append(['if-any', len(program
), f_args
, None])
329 # implied PRINT command
333 f_args
.append(_prepare_ref(arg
, for_names
, file_args
))
334 program
.append((self
._cmd
_format
, (f_args
[0], f_args
[1:])))
336 program
.append((self
._cmd
_print
,
337 _prepare_ref(args
[0], for_names
, file_args
)))
340 ### would be nice to say which blocks...
341 raise UnclosedBlocksError()
344 def _execute(self
, program
, fp
, ctx
):
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
)
355 def _cmd_print(self
, valref
, fp
, ctx
):
356 value
= _get_value(valref
, ctx
)
358 # if the value has a 'read' attribute, then it is a stream: copy it
359 if hasattr(value
, 'read'):
361 chunk
= value
.read(16384)
368 def _cmd_format(self
, (valref
, args
), fp
, ctx
):
369 fmt
= _get_value(valref
, ctx
)
370 parts
= _re_subst
.split(fmt
)
371 for i
in range(len(parts
)):
373 if i
%2 == 1 and piece
!= '%':
376 piece
= _get_value(args
[idx
], ctx
)
381 def _cmd_include(self
, (valref
, reader
), fp
, ctx
):
382 fname
= _get_value(valref
, ctx
)
383 ### note: we don't have the set of for_names to pass into this parse.
384 ### I don't think there is anything to do but document it.
385 self
._execute
(self
._parse
(reader
.read_other(fname
)), fp
, ctx
)
387 def _cmd_if_any(self
, args
, fp
, ctx
):
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
:
392 if _get_value(valref
, ctx
):
395 self
._do
_if
(value
, t_section
, f_section
, fp
, ctx
)
397 def _cmd_if_index(self
, args
, 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
)
412 def _cmd_is(self
, args
, fp
, ctx
):
413 ((left_ref
, right_ref
), t_section
, f_section
) = args
414 value
= _get_value(right_ref
, ctx
)
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:
427 self
._execute
(section
, fp
, ctx
)
429 def _cmd_for(self
, args
, fp
, ctx
):
430 ((valref
,), unused
, section
) = args
431 list = _get_value(valref
, ctx
)
432 if isinstance(list, StringType
):
433 raise NeedSequenceError()
435 ctx
.for_index
[refname
] = idx
= [ list, 0 ]
437 self
._execute
(section
, fp
, ctx
)
439 del ctx
.for_index
[refname
]
442 "Return a value suitable for [if-any bool_var] usage in a template."
448 def _prepare_ref(refname
, for_names
, file_args
):
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.
455 # is the reference a string constant?
456 if refname
[0] == '"':
457 return None, refname
[1:-1], None
459 # if this is an include-argument, then just return the prepared ref
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
):
473 # check if the next part is also a "for name"
474 name
= start
+ '.' + rest
[0]
475 if name
in for_names
:
480 return refname
, start
, rest
482 def _get_value((refname
, start
, rest
), ctx
):
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
491 # it was a string constant
493 if ctx
.for_index
.has_key(start
):
494 list, idx
= ctx
.for_index
[start
]
496 elif ctx
.data
.has_key(start
):
499 raise UnknownReference(refname
)
501 # walk the rest of the dotted reference
504 ob
= getattr(ob
, attr
)
505 except AttributeError:
506 raise UnknownReference(refname
)
508 # make sure we return a string instead of some various Python types
509 if isinstance(ob
, IntType
) or isinstance(ob
, FloatType
):
514 # string or a sequence
519 """A container for the execution context"""
523 "Abstract class which allows EZT to detect Reader objects."
525 class _FileReader(Reader
):
526 """Reads templates from the filesystem."""
527 def __init__(self
, fname
):
528 self
.text
= open(fname
, 'rb').read()
529 self
._dir
= os
.path
.dirname(fname
)
530 def read_other(self
, relative
):
531 return _FileReader(os
.path
.join(self
._dir
, relative
))
533 class _TextReader(Reader
):
534 """'Reads' a template from provided text."""
535 def __init__(self
, text
):
537 def read_other(self
, relative
):
538 raise BaseUnavailableError()
541 class EZTException(Exception):
542 """Parent class of all EZT exceptions."""
544 class ArgCountSyntaxError(EZTException
):
545 """A bracket directive got the wrong number of arguments."""
547 class UnknownReference(EZTException
):
548 """The template references an object not contained in the data dictionary."""
550 class NeedSequenceError(EZTException
):
551 """The object dereferenced by the template is no sequence (tuple or list)."""
553 class UnclosedBlocksError(EZTException
):
554 """This error may be simply a missing [end]."""
556 class UnmatchedEndError(EZTException
):
557 """This error may be caused by a misspelled if directive."""
559 class BaseUnavailableError(EZTException
):
560 """Base location is unavailable, which disables includes."""
563 # --- standard test environment ---
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__":
583 # invoke unit test for this module:
585 sys
.exit(_test(sys
.argv
)[0])