Projet

Général

Profil

Télécharger (25 ko) Statistiques
| Branche: | Révision:

root / larpe / trunk / ezt.py @ 8843f79b

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
   &quot; and &gt;)
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])
(6-6/20)