1
|
#!/usr/bin/env python
|
2
|
"""ezt.py -- easy templating
|
3
|
|
4
|
ezt templates are simply text files in whatever format you so desire
|
5
|
(such as XML, HTML, etc.) which contain directives sprinkled
|
6
|
throughout. With these directives it is possible to generate the
|
7
|
dynamic content from the ezt templates.
|
8
|
|
9
|
These directives are enclosed in square brackets. If you are a
|
10
|
C-programmer, you might be familar with the #ifdef directives of the C
|
11
|
preprocessor 'cpp'. ezt provides a similar concept. Additionally EZT
|
12
|
has a 'for' directive, which allows it to iterate (repeat) certain
|
13
|
subsections of the template according to sequence of data items
|
14
|
provided by the application.
|
15
|
|
16
|
The final rendering is performed by the method generate() of the Template
|
17
|
class. Building template instances can either be done using external
|
18
|
EZT files (convention: use the suffix .ezt for such files):
|
19
|
|
20
|
>>> template = Template("../templates/log.ezt")
|
21
|
|
22
|
or by calling the parse() method of a template instance directly with
|
23
|
a EZT template string:
|
24
|
|
25
|
>>> template = Template()
|
26
|
>>> template.parse('''<html><head>
|
27
|
... <title>[title_string]</title></head>
|
28
|
... <body><h1>[title_string]</h1>
|
29
|
... [for a_sequence] <p>[a_sequence]</p>
|
30
|
... [end] <hr>
|
31
|
... The [person] is [if-any state]in[else]out[end].
|
32
|
... </body>
|
33
|
... </html>
|
34
|
... ''')
|
35
|
|
36
|
The application should build a dictionary 'data' and pass it together
|
37
|
with the output fileobject to the templates generate method:
|
38
|
|
39
|
>>> data = {'title_string' : "A Dummy Page",
|
40
|
... 'a_sequence' : ['list item 1', 'list item 2', 'another element'],
|
41
|
... 'person': "doctor",
|
42
|
... 'state' : None }
|
43
|
>>> import sys
|
44
|
>>> template.generate(sys.stdout, data)
|
45
|
<html><head>
|
46
|
<title>A Dummy Page</title></head>
|
47
|
<body><h1>A Dummy Page</h1>
|
48
|
<p>list item 1</p>
|
49
|
<p>list item 2</p>
|
50
|
<p>another element</p>
|
51
|
<hr>
|
52
|
The doctor is out.
|
53
|
</body>
|
54
|
</html>
|
55
|
|
56
|
Template syntax error reporting should be improved. Currently it is
|
57
|
very sparse (template line numbers would be nice):
|
58
|
|
59
|
>>> Template().parse("[if-any where] foo [else] bar [end unexpected args]")
|
60
|
Traceback (innermost last):
|
61
|
File "<stdin>", line 1, in ?
|
62
|
File "ezt.py", line 220, in parse
|
63
|
self.program = self._parse(text)
|
64
|
File "ezt.py", line 275, in _parse
|
65
|
raise ArgCountSyntaxError(str(args[1:]))
|
66
|
ArgCountSyntaxError: ['unexpected', 'args']
|
67
|
>>> Template().parse("[if unmatched_end]foo[end]")
|
68
|
Traceback (innermost last):
|
69
|
File "<stdin>", line 1, in ?
|
70
|
File "ezt.py", line 206, in parse
|
71
|
self.program = self._parse(text)
|
72
|
File "ezt.py", line 266, in _parse
|
73
|
raise UnmatchedEndError()
|
74
|
UnmatchedEndError
|
75
|
|
76
|
|
77
|
Directives
|
78
|
==========
|
79
|
|
80
|
Several directives allow the use of dotted qualified names refering to objects
|
81
|
or attributes of objects contained in the data dictionary given to the
|
82
|
.generate() method.
|
83
|
|
84
|
Qualified names
|
85
|
---------------
|
86
|
|
87
|
Qualified names have two basic forms: a variable reference, or a string
|
88
|
constant. References are a name from the data dictionary with optional
|
89
|
dotted attributes (where each intermediary is an object with attributes,
|
90
|
of course).
|
91
|
|
92
|
Examples:
|
93
|
|
94
|
[varname]
|
95
|
|
96
|
[ob.attr]
|
97
|
|
98
|
["string"]
|
99
|
|
100
|
Simple directives
|
101
|
-----------------
|
102
|
|
103
|
[QUAL_NAME]
|
104
|
|
105
|
This directive is simply replaced by the value of the qualified name.
|
106
|
If the value is a number it's converted to a string before being
|
107
|
outputted. If it is None, nothing is outputted. If it is a python file
|
108
|
object (i.e. any object with a "read" method), it's contents are
|
109
|
outputted. If it is a callback function (any callable python object
|
110
|
is assumed to be a callback function), it is invoked and passed an EZT
|
111
|
printer function as an argument.
|
112
|
|
113
|
[QUAL_NAME QUAL_NAME ...]
|
114
|
|
115
|
If the first value is a callback function, it is invoked with the
|
116
|
output file pointer as a first argument, and the rest of the values as
|
117
|
additional arguments.
|
118
|
|
119
|
Otherwise, the first value defines a substitution format, specifying
|
120
|
constant text and indices of the additional arguments. The arguments
|
121
|
are substituted and the result is inserted into the output stream.
|
122
|
|
123
|
Example:
|
124
|
["abc %0 def %1 ghi %0" foo bar.baz]
|
125
|
|
126
|
Note that the first value can be any type of qualified name -- a string
|
127
|
constant or a variable reference. Use %% to substitute a percent sign.
|
128
|
Argument indices are 0-based.
|
129
|
|
130
|
[include "filename"] or [include QUAL_NAME]
|
131
|
|
132
|
This directive is replaced by content of the named include file. Note
|
133
|
that a string constant is more efficient -- the target file is compiled
|
134
|
inline. In the variable form, the target file is compiled and executed
|
135
|
at runtime.
|
136
|
|
137
|
Block directives
|
138
|
----------------
|
139
|
|
140
|
[for QUAL_NAME] ... [end]
|
141
|
|
142
|
The text within the [for ...] directive and the corresponding [end]
|
143
|
is repeated for each element in the sequence referred to by the
|
144
|
qualified name in the for directive. Within the for block this
|
145
|
identifiers now refers to the actual item indexed by this loop
|
146
|
iteration.
|
147
|
|
148
|
[if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end]
|
149
|
|
150
|
Test if any QUAL_NAME value is not None or an empty string or list.
|
151
|
The [else] clause is optional. CAUTION: Numeric values are
|
152
|
converted to string, so if QUAL_NAME refers to a numeric value 0,
|
153
|
the then-clause is substituted!
|
154
|
|
155
|
[if-index INDEX_FROM_FOR odd] ... [else] ... [end]
|
156
|
[if-index INDEX_FROM_FOR even] ... [else] ... [end]
|
157
|
[if-index INDEX_FROM_FOR first] ... [else] ... [end]
|
158
|
[if-index INDEX_FROM_FOR last] ... [else] ... [end]
|
159
|
[if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end]
|
160
|
|
161
|
These five directives work similar to [if-any], but are only useful
|
162
|
within a [for ...]-block (see above). The odd/even directives are
|
163
|
for example useful to choose different background colors for
|
164
|
adjacent rows in a table. Similar the first/last directives might
|
165
|
be used to remove certain parts (for example "Diff to previous"
|
166
|
doesn't make sense, if there is no previous).
|
167
|
|
168
|
[is QUAL_NAME STRING] ... [else] ... [end]
|
169
|
[is QUAL_NAME QUAL_NAME] ... [else] ... [end]
|
170
|
|
171
|
The [is ...] directive is similar to the other conditional
|
172
|
directives above. But it allows to compare two value references or
|
173
|
a value reference with some constant string.
|
174
|
|
175
|
[define VARIABLE] ... [end]
|
176
|
|
177
|
The [define ...] directive allows you to create and modify template
|
178
|
variables from within the template itself. Essentially, any data
|
179
|
between inside the [define ...] and its matching [end] will be
|
180
|
expanded using the other template parsing and output generation
|
181
|
rules, and then stored as a string value assigned to the variable
|
182
|
VARIABLE. The new (or changed) variable is then available for use
|
183
|
with other mechanisms such as [is ...] or [if-any ...], as long as
|
184
|
they appear later in the template.
|
185
|
|
186
|
[format STRING] ... [end]
|
187
|
|
188
|
The format directive controls how the values substituted into
|
189
|
templates are escaped before they are put into the output stream. It
|
190
|
has no effect on the literal text of the templates, only the output
|
191
|
from [QUAL_NAME ...] directives. STRING can be one of "raw" "html"
|
192
|
or "xml". The "raw" mode leaves the output unaltered. The "html" and
|
193
|
"xml" modes escape special characters using entity escapes (like
|
194
|
" and >)
|
195
|
"""
|
196
|
#
|
197
|
# Copyright (C) 2001-2005 Greg Stein. All Rights Reserved.
|
198
|
#
|
199
|
# Redistribution and use in source and binary forms, with or without
|
200
|
# modification, are permitted provided that the following conditions are
|
201
|
# met:
|
202
|
#
|
203
|
# * Redistributions of source code must retain the above copyright
|
204
|
# notice, this list of conditions and the following disclaimer.
|
205
|
#
|
206
|
# * Redistributions in binary form must reproduce the above copyright
|
207
|
# notice, this list of conditions and the following disclaimer in the
|
208
|
# documentation and/or other materials provided with the distribution.
|
209
|
#
|
210
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
211
|
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
212
|
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
213
|
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
|
214
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
215
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
216
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
217
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
218
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
219
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
220
|
# POSSIBILITY OF SUCH DAMAGE.
|
221
|
#
|
222
|
#
|
223
|
# This software is maintained by Greg and is available at:
|
224
|
# http://svn.webdav.org/repos/projects/ezt/trunk/
|
225
|
#
|
226
|
|
227
|
import string
|
228
|
import re
|
229
|
from types import StringType, IntType, FloatType, LongType
|
230
|
import os
|
231
|
import cgi
|
232
|
try:
|
233
|
import cStringIO
|
234
|
except ImportError:
|
235
|
import StringIO
|
236
|
cStringIO = StringIO
|
237
|
|
238
|
#
|
239
|
# Formatting types
|
240
|
#
|
241
|
FORMAT_RAW = 'raw'
|
242
|
FORMAT_HTML = 'html'
|
243
|
FORMAT_XML = 'xml'
|
244
|
|
245
|
#
|
246
|
# This regular expression matches three alternatives:
|
247
|
# expr: DIRECTIVE | BRACKET | COMMENT
|
248
|
# DIRECTIVE: '[' ITEM (whitespace ITEM)* ']
|
249
|
# ITEM: STRING | NAME
|
250
|
# STRING: '"' (not-slash-or-dquote | '\' anychar)* '"'
|
251
|
# NAME: (alphanum | '_' | '-' | '.')+
|
252
|
# BRACKET: '[[]'
|
253
|
# COMMENT: '[#' not-rbracket* ']'
|
254
|
#
|
255
|
# When used with the split() method, the return value will be composed of
|
256
|
# non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since
|
257
|
# the COMMENT matches are not placed into a group, they are considered a
|
258
|
# "splitting" value and simply dropped.
|
259
|
#
|
260
|
_item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)'
|
261
|
_re_parse = re.compile(r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item))
|
262
|
|
263
|
_re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+')
|
264
|
|
265
|
# block commands and their argument counts
|
266
|
_block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 }
|
267
|
_block_cmds = _block_cmd_specs.keys()
|
268
|
|
269
|
# two regular expresssions for compressing whitespace. the first is used to
|
270
|
# compress any whitespace including a newline into a single newline. the
|
271
|
# second regex is used to compress runs of whitespace into a single space.
|
272
|
_re_newline = re.compile('[ \t\r\f\v]*\n\\s*')
|
273
|
_re_whitespace = re.compile(r'\s\s+')
|
274
|
|
275
|
# this regex is used to substitute arguments into a value. we split the value,
|
276
|
# replace the relevant pieces, and then put it all back together. splitting
|
277
|
# will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or
|
278
|
# an integer.
|
279
|
_re_subst = re.compile('%(%|[0-9]+)')
|
280
|
|
281
|
class Template:
|
282
|
|
283
|
_printers = {
|
284
|
FORMAT_RAW : '_cmd_print',
|
285
|
FORMAT_HTML : '_cmd_print_html',
|
286
|
FORMAT_XML : '_cmd_print_xml',
|
287
|
}
|
288
|
|
289
|
def __init__(self, fname=None, compress_whitespace=1,
|
290
|
base_format=FORMAT_RAW):
|
291
|
self.compress_whitespace = compress_whitespace
|
292
|
if fname:
|
293
|
self.parse_file(fname, base_format)
|
294
|
|
295
|
def parse_file(self, fname, base_format=FORMAT_RAW):
|
296
|
"fname -> a string object with pathname of file containg an EZT template."
|
297
|
|
298
|
self.parse(_FileReader(fname), base_format)
|
299
|
|
300
|
def parse(self, text_or_reader, base_format=FORMAT_RAW):
|
301
|
"""Parse the template specified by text_or_reader.
|
302
|
|
303
|
The argument should be a string containing the template, or it should
|
304
|
specify a subclass of ezt.Reader which can read templates. The base
|
305
|
format for printing values is given by base_format.
|
306
|
"""
|
307
|
if not isinstance(text_or_reader, Reader):
|
308
|
# assume the argument is a plain text string
|
309
|
text_or_reader = _TextReader(text_or_reader)
|
310
|
|
311
|
printer = getattr(self, self._printers[base_format])
|
312
|
self.program = self._parse(text_or_reader, base_printer=printer)
|
313
|
|
314
|
def generate(self, fp, data):
|
315
|
if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)):
|
316
|
# a dictionary-like object was passed. convert it to an
|
317
|
# attribute-based object.
|
318
|
class _data_ob:
|
319
|
def __init__(self, d):
|
320
|
vars(self).update(d)
|
321
|
data = _data_ob(data)
|
322
|
|
323
|
ctx = _context()
|
324
|
ctx.data = data
|
325
|
ctx.for_index = { }
|
326
|
ctx.defines = { }
|
327
|
self._execute(self.program, fp, ctx)
|
328
|
|
329
|
def _parse(self, reader, for_names=None, file_args=(), base_printer=None):
|
330
|
"""text -> string object containing the template.
|
331
|
|
332
|
This is a private helper function doing the real work for method parse.
|
333
|
It returns the parsed template as a 'program'. This program is a sequence
|
334
|
made out of strings or (function, argument) 2-tuples.
|
335
|
|
336
|
Note: comment directives [# ...] are automatically dropped by _re_parse.
|
337
|
"""
|
338
|
|
339
|
# parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT
|
340
|
parts = _re_parse.split(reader.text)
|
341
|
|
342
|
program = [ ]
|
343
|
stack = [ ]
|
344
|
if not for_names:
|
345
|
for_names = [ ]
|
346
|
|
347
|
if base_printer:
|
348
|
printers = [ base_printer ]
|
349
|
else:
|
350
|
printers = [ self._cmd_print ]
|
351
|
|
352
|
for i in range(len(parts)):
|
353
|
piece = parts[i]
|
354
|
which = i % 3 # discriminate between: TEXT DIRECTIVE BRACKET
|
355
|
if which == 0:
|
356
|
# TEXT. append if non-empty.
|
357
|
if piece:
|
358
|
if self.compress_whitespace:
|
359
|
piece = _re_whitespace.sub(' ', _re_newline.sub('\n', piece))
|
360
|
program.append(piece)
|
361
|
elif which == 2:
|
362
|
# BRACKET directive. append '[' if present.
|
363
|
if piece:
|
364
|
program.append('[')
|
365
|
elif piece:
|
366
|
# DIRECTIVE is present.
|
367
|
args = _re_args.findall(piece)
|
368
|
cmd = args[0]
|
369
|
if cmd == 'else':
|
370
|
if len(args) > 1:
|
371
|
raise ArgCountSyntaxError(str(args[1:]))
|
372
|
### check: don't allow for 'for' cmd
|
373
|
idx = stack[-1][1]
|
374
|
true_section = program[idx:]
|
375
|
del program[idx:]
|
376
|
stack[-1][3] = true_section
|
377
|
elif cmd == 'end':
|
378
|
if len(args) > 1:
|
379
|
raise ArgCountSyntaxError(str(args[1:]))
|
380
|
# note: true-section may be None
|
381
|
try:
|
382
|
cmd, idx, args, true_section = stack.pop()
|
383
|
except IndexError:
|
384
|
raise UnmatchedEndError()
|
385
|
else_section = program[idx:]
|
386
|
if cmd == 'format':
|
387
|
printers.pop()
|
388
|
else:
|
389
|
func = getattr(self, '_cmd_' + re.sub('-', '_', cmd))
|
390
|
program[idx:] = [ (func, (args, true_section, else_section)) ]
|
391
|
if cmd == 'for':
|
392
|
for_names.pop()
|
393
|
elif cmd in _block_cmds:
|
394
|
if len(args) > _block_cmd_specs[cmd] + 1:
|
395
|
raise ArgCountSyntaxError(str(args[1:]))
|
396
|
### this assumes arg1 is always a ref unless cmd is 'define'
|
397
|
if cmd != 'define':
|
398
|
args[1] = _prepare_ref(args[1], for_names, file_args)
|
399
|
|
400
|
# handle arg2 for the 'is' command
|
401
|
if cmd == 'is':
|
402
|
args[2] = _prepare_ref(args[2], for_names, file_args)
|
403
|
elif cmd == 'for':
|
404
|
for_names.append(args[1][0]) # append the refname
|
405
|
elif cmd == 'format':
|
406
|
if args[1][0]:
|
407
|
raise BadFormatConstantError(str(args[1:]))
|
408
|
funcname = self._printers.get(args[1][1])
|
409
|
if not funcname:
|
410
|
raise UnknownFormatConstantError(str(args[1:]))
|
411
|
printers.append(getattr(self, funcname))
|
412
|
|
413
|
# remember the cmd, current pos, args, and a section placeholder
|
414
|
stack.append([cmd, len(program), args[1:], None])
|
415
|
elif cmd == 'include':
|
416
|
if args[1][0] == '"':
|
417
|
include_filename = args[1][1:-1]
|
418
|
f_args = [ ]
|
419
|
for arg in args[2:]:
|
420
|
f_args.append(_prepare_ref(arg, for_names, file_args))
|
421
|
program.extend(self._parse(reader.read_other(include_filename),
|
422
|
for_names, f_args, printers[-1]))
|
423
|
else:
|
424
|
if len(args) != 2:
|
425
|
raise ArgCountSyntaxError(str(args))
|
426
|
program.append((self._cmd_include,
|
427
|
(_prepare_ref(args[1], for_names, file_args),
|
428
|
reader)))
|
429
|
elif cmd == 'if-any':
|
430
|
f_args = [ ]
|
431
|
for arg in args[1:]:
|
432
|
f_args.append(_prepare_ref(arg, for_names, file_args))
|
433
|
stack.append(['if-any', len(program), f_args, None])
|
434
|
else:
|
435
|
# implied PRINT command
|
436
|
f_args = [ ]
|
437
|
for arg in args:
|
438
|
f_args.append(_prepare_ref(arg, for_names, file_args))
|
439
|
program.append((printers[-1], f_args))
|
440
|
|
441
|
if stack:
|
442
|
### would be nice to say which blocks...
|
443
|
raise UnclosedBlocksError()
|
444
|
return program
|
445
|
|
446
|
def _execute(self, program, fp, ctx):
|
447
|
"""This private helper function takes a 'program' sequence as created
|
448
|
by the method '_parse' and executes it step by step. strings are written
|
449
|
to the file object 'fp' and functions are called.
|
450
|
"""
|
451
|
for step in program:
|
452
|
if isinstance(step, StringType):
|
453
|
fp.write(step)
|
454
|
else:
|
455
|
step[0](step[1], fp, ctx)
|
456
|
|
457
|
def _cmd_print(self, valref, fp, ctx):
|
458
|
_write_value(valref, fp, ctx)
|
459
|
|
460
|
def _cmd_print_html(self, valref, fp, ctx):
|
461
|
_write_value(valref, fp, ctx, cgi.escape)
|
462
|
|
463
|
def _cmd_print_xml(self, valref, fp, ctx):
|
464
|
### use the same quoting as HTML for now
|
465
|
self._cmd_print_html(valref, fp, ctx)
|
466
|
|
467
|
def _cmd_include(self, (valref, reader), fp, ctx):
|
468
|
fname = _get_value(valref, ctx)
|
469
|
### note: we don't have the set of for_names to pass into this parse.
|
470
|
### I don't think there is anything to do but document it. we also
|
471
|
### don't have a current format (since that is a compile-time concept).
|
472
|
self._execute(self._parse(reader.read_other(fname)), fp, ctx)
|
473
|
|
474
|
def _cmd_if_any(self, args, fp, ctx):
|
475
|
"If any value is a non-empty string or non-empty list, then T else F."
|
476
|
(valrefs, t_section, f_section) = args
|
477
|
value = 0
|
478
|
for valref in valrefs:
|
479
|
if _get_value(valref, ctx):
|
480
|
value = 1
|
481
|
break
|
482
|
self._do_if(value, t_section, f_section, fp, ctx)
|
483
|
|
484
|
def _cmd_if_index(self, args, fp, ctx):
|
485
|
((valref, value), t_section, f_section) = args
|
486
|
list, idx = ctx.for_index[valref[0]]
|
487
|
if value == 'even':
|
488
|
value = idx % 2 == 0
|
489
|
elif value == 'odd':
|
490
|
value = idx % 2 == 1
|
491
|
elif value == 'first':
|
492
|
value = idx == 0
|
493
|
elif value == 'last':
|
494
|
value = idx == len(list)-1
|
495
|
else:
|
496
|
value = idx == int(value)
|
497
|
self._do_if(value, t_section, f_section, fp, ctx)
|
498
|
|
499
|
def _cmd_is(self, args, fp, ctx):
|
500
|
((left_ref, right_ref), t_section, f_section) = args
|
501
|
value = _get_value(right_ref, ctx)
|
502
|
value = string.lower(_get_value(left_ref, ctx)) == string.lower(value)
|
503
|
self._do_if(value, t_section, f_section, fp, ctx)
|
504
|
|
505
|
def _do_if(self, value, t_section, f_section, fp, ctx):
|
506
|
if t_section is None:
|
507
|
t_section = f_section
|
508
|
f_section = None
|
509
|
if value:
|
510
|
section = t_section
|
511
|
else:
|
512
|
section = f_section
|
513
|
if section is not None:
|
514
|
self._execute(section, fp, ctx)
|
515
|
|
516
|
def _cmd_for(self, args, fp, ctx):
|
517
|
((valref,), unused, section) = args
|
518
|
list = _get_value(valref, ctx)
|
519
|
if isinstance(list, StringType):
|
520
|
raise NeedSequenceError()
|
521
|
refname = valref[0]
|
522
|
ctx.for_index[refname] = idx = [ list, 0 ]
|
523
|
for item in list:
|
524
|
self._execute(section, fp, ctx)
|
525
|
idx[1] = idx[1] + 1
|
526
|
del ctx.for_index[refname]
|
527
|
|
528
|
def _cmd_define(self, args, fp, ctx):
|
529
|
((name,), unused, section) = args
|
530
|
valfp = cStringIO.StringIO()
|
531
|
if section is not None:
|
532
|
self._execute(section, valfp, ctx)
|
533
|
ctx.defines[name] = valfp.getvalue()
|
534
|
|
535
|
def boolean(value):
|
536
|
"Return a value suitable for [if-any bool_var] usage in a template."
|
537
|
if value:
|
538
|
return 'yes'
|
539
|
return None
|
540
|
|
541
|
|
542
|
def _prepare_ref(refname, for_names, file_args):
|
543
|
"""refname -> a string containing a dotted identifier. example:"foo.bar.bang"
|
544
|
for_names -> a list of active for sequences.
|
545
|
|
546
|
Returns a `value reference', a 3-tuple made out of (refname, start, rest),
|
547
|
for fast access later.
|
548
|
"""
|
549
|
# is the reference a string constant?
|
550
|
if refname[0] == '"':
|
551
|
return None, refname[1:-1], None
|
552
|
|
553
|
parts = string.split(refname, '.')
|
554
|
start = parts[0]
|
555
|
rest = parts[1:]
|
556
|
|
557
|
# if this is an include-argument, then just return the prepared ref
|
558
|
if start[:3] == 'arg':
|
559
|
try:
|
560
|
idx = int(start[3:])
|
561
|
except ValueError:
|
562
|
pass
|
563
|
else:
|
564
|
if idx < len(file_args):
|
565
|
orig_refname, start, more_rest = file_args[idx]
|
566
|
if more_rest is None:
|
567
|
# the include-argument was a string constant
|
568
|
return None, start, None
|
569
|
|
570
|
# prepend the argument's "rest" for our further processing
|
571
|
rest[:0] = more_rest
|
572
|
|
573
|
# rewrite the refname to ensure that any potential 'for' processing
|
574
|
# has the correct name
|
575
|
### this can make it hard for debugging include files since we lose
|
576
|
### the 'argNNN' names
|
577
|
if not rest:
|
578
|
return start, start, [ ]
|
579
|
refname = start + '.' + string.join(rest, '.')
|
580
|
|
581
|
if for_names:
|
582
|
# From last to first part, check if this reference is part of a for loop
|
583
|
for i in range(len(parts), 0, -1):
|
584
|
name = string.join(parts[:i], '.')
|
585
|
if name in for_names:
|
586
|
return refname, name, parts[i:]
|
587
|
|
588
|
return refname, start, rest
|
589
|
|
590
|
def _get_value((refname, start, rest), ctx):
|
591
|
"""(refname, start, rest) -> a prepared `value reference' (see above).
|
592
|
ctx -> an execution context instance.
|
593
|
|
594
|
Does a name space lookup within the template name space. Active
|
595
|
for blocks take precedence over data dictionary members with the
|
596
|
same name.
|
597
|
"""
|
598
|
if rest is None:
|
599
|
# it was a string constant
|
600
|
return start
|
601
|
|
602
|
# get the starting object
|
603
|
if ctx.for_index.has_key(start):
|
604
|
list, idx = ctx.for_index[start]
|
605
|
ob = list[idx]
|
606
|
elif ctx.defines.has_key(start):
|
607
|
ob = ctx.defines[start]
|
608
|
elif hasattr(ctx.data, start):
|
609
|
ob = getattr(ctx.data, start)
|
610
|
else:
|
611
|
raise UnknownReference(refname)
|
612
|
|
613
|
# walk the rest of the dotted reference
|
614
|
for attr in rest:
|
615
|
try:
|
616
|
ob = getattr(ob, attr)
|
617
|
except AttributeError:
|
618
|
raise UnknownReference(refname)
|
619
|
|
620
|
# make sure we return a string instead of some various Python types
|
621
|
if isinstance(ob, IntType) \
|
622
|
or isinstance(ob, LongType) \
|
623
|
or isinstance(ob, FloatType):
|
624
|
return str(ob)
|
625
|
if ob is None:
|
626
|
return ''
|
627
|
|
628
|
# string or a sequence
|
629
|
return ob
|
630
|
|
631
|
def _write_value(valrefs, fp, ctx, format=lambda s: s):
|
632
|
value = _get_value(valrefs[0], ctx)
|
633
|
args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:])
|
634
|
|
635
|
# if the value has a 'read' attribute, then it is a stream: copy it
|
636
|
if hasattr(value, 'read'):
|
637
|
while 1:
|
638
|
chunk = value.read(16384)
|
639
|
if not chunk:
|
640
|
break
|
641
|
fp.write(format(chunk))
|
642
|
|
643
|
# value is a callback function: call with file pointer and extra args
|
644
|
elif callable(value):
|
645
|
apply(value, [fp] + args)
|
646
|
|
647
|
# value is a substitution pattern
|
648
|
elif args:
|
649
|
parts = _re_subst.split(value)
|
650
|
for i in range(len(parts)):
|
651
|
piece = parts[i]
|
652
|
if i%2 == 1 and piece != '%':
|
653
|
idx = int(piece)
|
654
|
if idx < len(args):
|
655
|
piece = args[idx]
|
656
|
else:
|
657
|
piece = '<undef>'
|
658
|
if format:
|
659
|
fp.write(format(piece))
|
660
|
|
661
|
# plain old value, write to output
|
662
|
else:
|
663
|
fp.write(format(value))
|
664
|
|
665
|
|
666
|
class _context:
|
667
|
"""A container for the execution context"""
|
668
|
|
669
|
|
670
|
class Reader:
|
671
|
"Abstract class which allows EZT to detect Reader objects."
|
672
|
|
673
|
class _FileReader(Reader):
|
674
|
"""Reads templates from the filesystem."""
|
675
|
def __init__(self, fname):
|
676
|
self.text = open(fname, 'rb').read()
|
677
|
self._dir = os.path.dirname(fname)
|
678
|
def read_other(self, relative):
|
679
|
return _FileReader(os.path.join(self._dir, relative))
|
680
|
|
681
|
class _TextReader(Reader):
|
682
|
"""'Reads' a template from provided text."""
|
683
|
def __init__(self, text):
|
684
|
self.text = text
|
685
|
def read_other(self, relative):
|
686
|
raise BaseUnavailableError()
|
687
|
|
688
|
|
689
|
class EZTException(Exception):
|
690
|
"""Parent class of all EZT exceptions."""
|
691
|
|
692
|
class ArgCountSyntaxError(EZTException):
|
693
|
"""A bracket directive got the wrong number of arguments."""
|
694
|
|
695
|
class UnknownReference(EZTException):
|
696
|
"""The template references an object not contained in the data dictionary."""
|
697
|
|
698
|
class NeedSequenceError(EZTException):
|
699
|
"""The object dereferenced by the template is no sequence (tuple or list)."""
|
700
|
|
701
|
class UnclosedBlocksError(EZTException):
|
702
|
"""This error may be simply a missing [end]."""
|
703
|
|
704
|
class UnmatchedEndError(EZTException):
|
705
|
"""This error may be caused by a misspelled if directive."""
|
706
|
|
707
|
class BaseUnavailableError(EZTException):
|
708
|
"""Base location is unavailable, which disables includes."""
|
709
|
|
710
|
class BadFormatConstantError(EZTException):
|
711
|
"""Format specifiers must be string constants."""
|
712
|
|
713
|
class UnknownFormatConstantError(EZTException):
|
714
|
"""The format specifier is an unknown value."""
|
715
|
|
716
|
|
717
|
# --- standard test environment ---
|
718
|
def test_parse():
|
719
|
assert _re_parse.split('[a]') == ['', '[a]', None, '']
|
720
|
assert _re_parse.split('[a] [b]') == \
|
721
|
['', '[a]', None, ' ', '[b]', None, '']
|
722
|
assert _re_parse.split('[a c] [b]') == \
|
723
|
['', '[a c]', None, ' ', '[b]', None, '']
|
724
|
assert _re_parse.split('x [a] y [b] z') == \
|
725
|
['x ', '[a]', None, ' y ', '[b]', None, ' z']
|
726
|
assert _re_parse.split('[a "b" c "d"]') == \
|
727
|
['', '[a "b" c "d"]', None, '']
|
728
|
assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \
|
729
|
['', '["a \\"b[foo]" c.d f]', None, '']
|
730
|
|
731
|
def _test(argv):
|
732
|
import doctest, ezt
|
733
|
verbose = "-v" in argv
|
734
|
return doctest.testmod(ezt, verbose=verbose)
|
735
|
|
736
|
if __name__ == "__main__":
|
737
|
# invoke unit test for this module:
|
738
|
import sys
|
739
|
sys.exit(_test(sys.argv)[0])
|