Author: gstein
Date: Tue Jun 11 22:33:50 2013
New Revision: 1491999

URL: http://svn.apache.org/r1491999
Log:
For the large portions of text from make_issue.pl, move them into
templates. They were Apache-specific, so couldn't stay in the source
files. Using EZT for template rendering.

make_issue.py isn't sending email yet, but that will be the next step.

* cmdline/make_issue.py:
  (): import cStringIO and ezt
  (main): pass more parameters into the working functions, so they can
    substitute the data into their outputs.
  (create_info_file): use the info-header template, which also inserts
    the args.file information (ie. simplify the code!)
  (build_hash): ensure all values are 2-tuples
  (verify_email): accept the issue_name for generating into the template
  (verify_voters): we don't need the extra newline. _basic_verify will
    handle that.
  (email_monitors): generate the message that will be sent. print it,
    but don't email yet.
  (_explain_vote): removed in favor of ...
  (_use_template): ... this. generates text basic on a template and
    input data.

* cmdline/templates/explain.ezt:
* cmdline/templates/info-header.ezt:
* cmdline/templates/monitor-email.ezt:
  (...): new templates for generating emails and other text output.

* lib/ezt.py:
  (...): small templating library. Category A.

Added:
    steve/trunk/cmdline/templates/
    steve/trunk/cmdline/templates/explain.ezt   (with props)
    steve/trunk/cmdline/templates/info-header.ezt   (with props)
    steve/trunk/cmdline/templates/monitor-email.ezt   (with props)
    steve/trunk/lib/ezt.py   (with props)
Modified:
    steve/trunk/cmdline/make_issue.py

Modified: steve/trunk/cmdline/make_issue.py
URL: 
http://svn.apache.org/viewvc/steve/trunk/cmdline/make_issue.py?rev=1491999&r1=1491998&r2=1491999&view=diff
==============================================================================
--- steve/trunk/cmdline/make_issue.py (original)
+++ steve/trunk/cmdline/make_issue.py Tue Jun 11 22:33:50 2013
@@ -40,10 +40,12 @@ import os
 import argparse
 import re
 import shutil
+import cStringIO
 
 ### how do we want to "properly" adjust path?
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 
'../lib')))
 import steve
+import ezt
 
 
 def main():
@@ -64,15 +66,15 @@ def main():
   # Note: config.issue_dir updated as a side-effect
   create_issue_dir(issue_name, args, config)
 
-  info_fname = create_info_file(args, config)
+  info_fname = create_info_file(issue_name, args, config)
   monitors_hash, hash = build_hash(info_fname, voters, args)
 
   monitors_fname = create_monitors_file(args, config)
   type_fname = create_type_file(info_fname, args, config)
-  verify_email(info_fname, args, config)
+  verify_email(issue_name, info_fname, args, config)
   verify_voters(voters, args, config)
 
-  email_monitors()
+  email_monitors(issue_name, info_fname, monitors_hash, hash, args, config)
   email_voters()
 
   print "Issue %s with hashcode of %s\nhas been successfully created." \
@@ -191,14 +193,12 @@ def create_issue_dir(issue_name, args, c
   os.mkdir(config.issue_dir, 0700)
 
 
-def create_info_file(args, config):
+def create_info_file(issue_name, args, config):
   info_fname = config.issue_dir + '/issue'
 
-  ### generate the contents
-  contents = '### contents\n\n'
-  info = open(args.file).read()
-
-  open(info_fname, 'w').write(contents + info)
+  contents = _use_template('templates/info-header.ezt', None, issue_name,
+                           None, None, None, args, config)
+  open(info_fname, 'w').write(contents)
 
   return info_fname
 
@@ -214,8 +214,8 @@ def build_hash(info_fname, voters, args)
       h2 = steve.get_hash_of('%s:%s' % (issue_id, h1))
 
       hash[voter] = (h1, h2)
-      hash[h1] = voter
-      hash[h2] = voter
+      hash[h1] = (voter, None)
+      hash[h2] = (voter, None)
 
     if len(hash) == 3 * len(voters):
       return monitors_hash, hash
@@ -248,17 +248,19 @@ def create_type_file(info_fname, args, c
   return type_fname
 
 
-def verify_email(info_fname, args, config):
+def verify_email(issue_name, info_fname, args, config):
   print 'Here is the issue information to be sent to each voter:'
 
-  contents = open(info_fname).read() + _explain_vote('unique-hash-key')
+  contents = open(info_fname).read() \
+             + _use_template('templates/explain.ezt', 'unique-hash-key', 
issue_name,
+                             None, None, None, args, config)
   _basic_verify(contents, args, config)
 
 
 def verify_voters(voters, args, config):
   print 'Here is the list of voter e-mail addresses:'
 
-  contents = '\n'.join(voters) + '\n'
+  contents = '\n'.join(voters)
   _basic_verify(contents, args, config)
 
 
@@ -281,16 +283,49 @@ def _basic_verify(contents, args, config
       return
 
 
-def email_monitors():
-  pass
+def email_monitors(issue_name, info_fname, monitors_hash, hash, args, config):
+  # Collect hash signatures of voter files that should not change
+  ### need to expand this. allow some to be missing during dev/test.
+  sigs = ['%s: %s' % (steve.hash_file('make_issue.py'), 'make_issue.py'),
+          ]
+
+  msg = _use_template('templates/monitor-email.ezt', 'unique-hash-key',
+                      issue_name, monitors_hash, hash, sigs,
+                      args, config)
+
+  ### mail the result. for now, print out what would have been mailed.
+  print '### DEBUG'
+  print msg
+  print '###'
 
 
 def email_voters():
   pass
 
 
-def _explain_vote(key):
-  return '### explain vote for: %s\n' % (key,)
+def _use_template(template_fname, key, issue_name, monitors_hash, hash, sigs,
+                  args, config):
+  data = {
+    'hashid': key,
+    'type': args.votetype.strip('0123456789'),
+    'issue_name': issue_name,
+    'group': args.group,
+    'selector': args.selector,
+    'style': args.style,
+    'hostname': config.hostname,
+    'monitors': args.monitors,
+    'monitors_hash': monitors_hash,
+    'email': config.email,
+    'file': args.file,
+    'sigs': sigs,
+    }
+  if hash:
+    data['count'] = len(hash)
+    data['hash2'] = sorted(v[1] for v in hash.values() if v[1])
+
+  buf = cStringIO.StringIO()
+  ezt.Template(template_fname, compress_whitespace=False).generate(buf, data)
+  return buf.getvalue()
 
 
 ### keep this? not sure that exceptions would be helpful since there is likely

Added: steve/trunk/cmdline/templates/explain.ezt
URL: 
http://svn.apache.org/viewvc/steve/trunk/cmdline/templates/explain.ezt?rev=1491999&view=auto
==============================================================================
--- steve/trunk/cmdline/templates/explain.ezt (added)
+++ steve/trunk/cmdline/templates/explain.ezt Tue Jun 11 22:33:50 2013
@@ -0,0 +1,77 @@
+[#
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+]= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+
+Your voting key for this issue: [hashid]
+
+In order to vote, either visit
+
+   https://vote.apache.org/cast/[issue_name]/[hashid]
+
+or use ssh to login to [hostname] and then run
+
+   /home/voter/bin/vote [issue_name] [hashid] "vote"
+[is type "yna"]
+where "vote" must be replaced by "yes", "no", or "abstain".
+[else][is type "stv"]
+where "vote" must be replaced by a single word containing the
+concatenated labels of candidates in the order that you wish them
+to be selected.  In other words, if you want to vote for the candidates
+labeled [[]x], [[]s], and [[]p], in that order, then your vote should be "xsp".
+
+This election will be decided according to the Single Transferable Vote
+rules described at
+
+   http://wiki.apache.org/general/BoardVoting
+   http://www.electoral-reform.org.uk/votingsystems/stvi.htm
+   http://www.cix.co.uk/~rosenstiel/stvrules/index.htm
+
+for an election with [selector] open slots. 
+
+You have one vote.  Use your vote by entering the label of your
+first preference candidate followed by, if desired, the label of your
+second preference candidate, and so on until you are indifferent about
+the remaining candidates.  The sequence of your preferences is crucial.
+You should continue to express preferences only as long as you are able
+to place successive candidates in order.  A later preference is considered
+only if an earlier preference has a surplus above the quota required for
+election, or is excluded because of insufficient support.  Under no
+circumstances will a later preference count against an earlier preference.
+
+You may list as many candidates as you wish, but no more than once per
+vote (e.g., "xsxp" would be rejected).
+[else]
+where "vote" must be replaced by a single word containing the
+concatenated labels of your [selector] choices.  In other words,
+if you want to vote for the candidates labeled [[]x], [[]s], and [[]p],
+then your vote should be "xsp" (order does not matter).
+[end][end]
+If for some reason you are unable to use ssh to access [hostname],
+then you can vote by proxy: simply send your voting key to some
+person with ssh access that you trust, preferably with instructions
+on how you wish them to place your vote.
+
+For verification purposes, you will be receiving an e-mail notification
+each time your voting key is used.  Repeat votes will be considered
+a complete replacement of your prior vote.  Your vote will be
+recorded in a tally file and sent to the vote monitors along with
+a different unique key, minimizing the chance that the contents of
+your vote will be accidentally seen by someone else while associated
+to you.  That is why the verification e-mail will only state that you
+have voted, rather than including how you voted.
+
+If you have any problems or questions, send a reply to the vote monitors
+for this issue: [monitors]

Propchange: steve/trunk/cmdline/templates/explain.ezt
------------------------------------------------------------------------------
    svn:eol-style = native

Added: steve/trunk/cmdline/templates/info-header.ezt
URL: 
http://svn.apache.org/viewvc/steve/trunk/cmdline/templates/info-header.ezt?rev=1491999&view=auto
==============================================================================
--- steve/trunk/cmdline/templates/info-header.ezt (added)
+++ steve/trunk/cmdline/templates/info-header.ezt Tue Jun 11 22:33:50 2013
@@ -0,0 +1,23 @@
+[#
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+]Hello Apache [group],
+
+A call for votes has been declared for the following:
+
+   Issue: [issue_name]
+   Voting style: [style]
+
+[insertfile file]

Propchange: steve/trunk/cmdline/templates/info-header.ezt
------------------------------------------------------------------------------
    svn:eol-style = native

Added: steve/trunk/cmdline/templates/monitor-email.ezt
URL: 
http://svn.apache.org/viewvc/steve/trunk/cmdline/templates/monitor-email.ezt?rev=1491999&view=auto
==============================================================================
--- steve/trunk/cmdline/templates/monitor-email.ezt (added)
+++ steve/trunk/cmdline/templates/monitor-email.ezt Tue Jun 11 22:33:50 2013
@@ -0,0 +1,59 @@
+[#
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+][#
+
+Note that we are generating header fields to push into Sendmail.
+
+]From: "Apache voting tool" <[email]>
+To: [monitors]
+Subject: Monitoring vote on [issue_name]
+
+You have been selected as a vote monitor for Apache
+
+   Issue: [issue_name]
+   Voting style: [style]
+
+[insertfile file]
+= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+
+The voting system will record a tally of the votes as they are received
+and will report that to you when the voting is closed.  To close voting,
+use ssh to login to $host and then run
+
+   /home/voter/bin/close_issue [issue_name] [monitors_hash]
+
+For verification purposes, you will be receiving an e-mail notification
+of each vote submitted by the voters.  Repeat votes should be considered
+a complete replacement of the person's prior vote.  Your primary role in
+all of this is to compare the votes you received with the results that
+are tallied automatically, letting the group know if there is a
+significant difference.  Any change made to the issue files during the
+voting process will immediately invalidate the hash IDs and kill voting.
+
+The following [count] voters are valid, as identified by the double-hashed ID
+that will be sent to you when a vote is recorded.  Note that these IDs are
+different from the single-hash IDs used by the voters when voting.
+
+[for hash2]  voter: [hash2]
+[end]
+The following explains the voting process to voters:
+
+[include "explain.ezt"]
+
+Current file digests:
+
+[for sigs][sigs]
+[end]

Propchange: steve/trunk/cmdline/templates/monitor-email.ezt
------------------------------------------------------------------------------
    svn:eol-style = native

Added: steve/trunk/lib/ezt.py
URL: http://svn.apache.org/viewvc/steve/trunk/lib/ezt.py?rev=1491999&view=auto
==============================================================================
--- steve/trunk/lib/ezt.py (added)
+++ steve/trunk/lib/ezt.py Tue Jun 11 22:33:50 2013
@@ -0,0 +1,657 @@
+#!/usr/bin/env python
+"""ezt.py -- EaZy Templating
+
+For documentation, please see: http://code.google.com/p/ezt/wiki/Syntax
+"""
+#
+# Copyright (C) 2001-2011 Greg Stein. All Rights Reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+#
+# This software is maintained by Greg and is available at:
+#    http://code.google.com/p/ezt/
+#
+
+__author__ = 'Greg Stein'
+__version__ = '1.0'
+__license__ = 'BSD'
+
+import re
+from types import IntType, FloatType, LongType
+import os
+import urllib
+import StringIO
+
+#
+# Formatting types
+#
+FORMAT_RAW = 'raw'
+FORMAT_HTML = 'html'
+FORMAT_XML = 'xml'
+FORMAT_JS = 'js'
+FORMAT_URL = 'url'
+
+#
+# This regular expression matches four alternatives:
+#   expr: NEWLINE | DIRECTIVE | BRACKET | COMMENT
+#   DIRECTIVE: '[' ITEM (whitespace ARG)* ']
+#   ITEM: STRING | NAME
+#   ARG: STRING | NAME | NUMBER
+#   STRING: '"' (not-slash-or-dquote | '\' anychar)* '"'
+#   NAME: (alpha | '_') (alphanum | '_' | '-' | '.')*
+#   NUMBER: digit+
+#   BRACKET: '[[]'
+#   COMMENT: '[#' not-rbracket* ']'
+#
+# Note: the above BNR is a bit loose around ITEM/ARG/NAME/NUMBER. The
+#       important point is that the first value in a directive must
+#       start with '_' or an alpha character (no digits). This greatly
+#       helps to avoid simple errors like '[0]' in templates.
+#
+# When used with the split() method, the return value will be composed of
+# non-matching text and the three paren groups (NEWLINE, DIRECTIVE and
+# BRACKET). Since the COMMENT matches are not placed into a group, they are
+# considered a "splitting" value and simply dropped.
+#
+_item = r'(?:"(?:[^\\"]|\\.)*"|[A-Za-z_][-\w.]*)'
+_arg = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)'
+_re_parse = re.compile(r'(\r?\n)|\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' %
+                       (_item, _arg))
+
+_re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+')
+
+# block commands and their argument counts
+_block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 }
+_block_cmds = _block_cmd_specs.keys()
+
+# two regular expressions for compressing whitespace. the first is used to
+# compress any whitespace including a newline into a single newline. the
+# second regex is used to compress runs of whitespace into a single space.
+_re_newline = re.compile('[ \t\r\f\v]*\n\\s*')
+_re_whitespace = re.compile(r'\s\s+')
+
+# this regex is used to substitute arguments into a value. we split the value,
+# replace the relevant pieces, and then put it all back together. splitting
+# will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or
+# an integer.
+_re_subst = re.compile('%(%|[0-9]+)')
+
+class Template:
+
+  def __init__(self, fname=None, compress_whitespace=1,
+               base_format=FORMAT_RAW):
+    self.compress_whitespace = compress_whitespace
+    if fname:
+      self.parse_file(fname, base_format)
+
+  def parse_file(self, fname, base_format=FORMAT_RAW):
+    "fname -> a string object with pathname of file containg an EZT template."
+
+    self.parse(_FileReader(fname), base_format)
+
+  def parse(self, text_or_reader, base_format=FORMAT_RAW):
+    """Parse the template specified by text_or_reader.
+
+    The argument should be a string containing the template, or it should
+    specify a subclass of ezt.Reader which can read templates. The base
+    format for printing values is given by base_format.
+    """
+    if not isinstance(text_or_reader, Reader):
+      # assume the argument is a plain text string
+      text_or_reader = _TextReader(text_or_reader)
+
+    self.program = self._parse(text_or_reader,
+                               base_printer=_parse_format(base_format))
+
+  def generate(self, fp, data):
+    if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)):
+      # a dictionary-like object was passed. convert it to an
+      # attribute-based object.
+      class _data_ob:
+        def __init__(self, d):
+          vars(self).update(d)
+      data = _data_ob(data)
+
+    ctx = _context()
+    ctx.data = data
+    ctx.for_index = { }
+    ctx.defines = { }
+    self._execute(self.program, fp, ctx)
+
+  def _parse(self, reader, for_names=None, file_args=(), base_printer=None):
+    """text -> string object containing the template.
+
+    This is a private helper function doing the real work for method parse.
+    It returns the parsed template as a 'program'.  This program is a sequence
+    made out of strings or (function, argument) 2-tuples.
+
+    Note: comment directives [# ...] are automatically dropped by _re_parse.
+    """
+
+    filename = reader.filename()
+    # parse the template program into: (TEXT NEWLINE DIRECTIVE BRACKET)* TEXT
+    parts = _re_parse.split(reader.text)
+
+    program = [ ]
+    stack = [ ]
+    if not for_names:
+      for_names = [ ]
+
+    if base_printer is None:
+      base_printer = ()
+    printers = [ base_printer ]
+
+    one_newline_copied = False
+    line_number = 1
+    for i in range(len(parts)):
+      piece = parts[i]
+      which = i % 4  # discriminate between: TEXT NEWLINE DIRECTIVE BRACKET
+      if which == 0:
+        # TEXT. append if non-empty.
+        if piece:
+          if self.compress_whitespace:
+            piece = _re_whitespace.sub(' ', piece)
+          program.append(piece)
+          one_newline_copied = False
+      elif which == 1:
+        # NEWLINE. append unless compress_whitespace requested
+        if piece:
+          line_number += 1
+          if self.compress_whitespace:
+            if not one_newline_copied:
+              program.append('\n')
+              one_newline_copied = True
+          else:
+            program.append(piece)
+      elif which == 3:
+        # BRACKET directive. append '[' if present.
+        if piece:
+          program.append('[')
+          one_newline_copied = False
+      elif piece:
+        # DIRECTIVE is present.
+        one_newline_copied = False
+        args = _re_args.findall(piece)
+        cmd = args[0]
+        if cmd == 'else':
+          if len(args) > 1:
+            raise ArgCountSyntaxError(str(args[1:]), filename, line_number)
+          ### check: don't allow for 'for' cmd
+          idx = stack[-1][1]
+          true_section = program[idx:]
+          del program[idx:]
+          stack[-1][3] = true_section
+        elif cmd == 'end':
+          if len(args) > 1:
+            raise ArgCountSyntaxError(str(args[1:]), filename, line_number)
+          # note: true-section may be None
+          try:
+            cmd, idx, args, true_section, start_line_number = stack.pop()
+          except IndexError:
+            raise UnmatchedEndError(None, filename, line_number)
+          else_section = program[idx:]
+          if cmd == 'format':
+            printers.pop()
+          else:
+            func = getattr(self, '_cmd_' + re.sub('-', '_', cmd))
+            program[idx:] = [ (func, (args, true_section, else_section),
+                               filename, line_number) ]
+            if cmd == 'for':
+              for_names.pop()
+        elif cmd in _block_cmds:
+          if len(args) > _block_cmd_specs[cmd] + 1:
+            raise ArgCountSyntaxError(str(args[1:]), filename, line_number)
+          ### this assumes arg1 is always a ref unless cmd is 'define'
+          if cmd != 'define':
+            args[1] = _prepare_ref(args[1], for_names, file_args)
+
+          # handle arg2 for the 'is' command
+          if cmd == 'is':
+            args[2] = _prepare_ref(args[2], for_names, file_args)
+          elif cmd == 'for':
+            for_names.append(args[1][0])  # append the refname
+          elif cmd == 'format':
+            if args[1][0]:
+              raise BadFormatConstantError(str(args[1:]), filename, 
line_number)
+            printers.append(_parse_format(args[1][1]))
+
+          # remember the cmd, current pos, args, and a section placeholder
+          stack.append([cmd, len(program), args[1:], None, line_number])
+        elif cmd == 'include' or cmd == 'insertfile':
+          is_insertfile = (cmd == 'insertfile')
+          # extra arguments are meaningless when using insertfile
+          if is_insertfile and len(args) != 2:
+            raise ArgCountSyntaxError(str(args), filename, line_number)
+          if args[1][0] == '"':
+            include_filename = args[1][1:-1]
+            if is_insertfile:
+              program.append(reader.read_other(include_filename).text)
+            else:
+              f_args = [ ]
+              for arg in args[2:]:
+                f_args.append(_prepare_ref(arg, for_names, file_args))
+              program.extend(self._parse(reader.read_other(include_filename),
+                                         for_names, f_args, printers[-1]))
+          else:
+            if len(args) != 2:
+              raise ArgCountSyntaxError(str(args), filename, line_number)
+            if is_insertfile:
+              cmd = self._cmd_insertfile
+            else:
+              cmd = self._cmd_include
+            program.append((cmd,
+                            (_prepare_ref(args[1], for_names, file_args),
+                             reader, printers[-1]), filename, line_number))
+        elif cmd == 'if-any':
+          f_args = [ ]
+          for arg in args[1:]:
+            f_args.append(_prepare_ref(arg, for_names, file_args))
+          stack.append(['if-any', len(program), f_args, None, line_number])
+        else:
+          # implied PRINT command
+          if len(args) > 1:
+            f_args = [ ]
+            for arg in args:
+              f_args.append(_prepare_ref(arg, for_names, file_args))
+            program.append((self._cmd_subst,
+                            (printers[-1], f_args[0], f_args[1:]),
+                            filename, line_number))
+          else:
+            valref = _prepare_ref(args[0], for_names, file_args)
+            program.append((self._cmd_print, (printers[-1], valref),
+                            filename, line_number))
+
+    if stack:
+      raise UnclosedBlocksError('Block opened at line %s' % stack[-1][4],
+                                filename=filename)
+    return program
+
+  def _execute(self, program, fp, ctx):
+    """This private helper function takes a 'program' sequence as created
+    by the method '_parse' and executes it step by step.  strings are written
+    to the file object 'fp' and functions are called.
+    """
+    for step in program:
+      if isinstance(step, basestring):
+        fp.write(step)
+      else:
+        method, method_args, filename, line_number = step
+        method(method_args, fp, ctx, filename, line_number)
+
+  def _cmd_print(self, (transforms, valref), fp, ctx, filename, line_number):
+    value = _get_value(valref, ctx, filename, line_number)
+    # if the value has a 'read' attribute, then it is a stream: copy it
+    if hasattr(value, 'read'):
+      while 1:
+        chunk = value.read(16384)
+        if not chunk:
+          break
+        for t in transforms:
+          chunk = t(chunk)
+        fp.write(chunk)
+    else:
+      for t in transforms:
+        value = t(value)
+      fp.write(value)
+
+  def _cmd_subst(self, (transforms, valref, args), fp, ctx, filename,
+                 line_number):
+    fmt = _get_value(valref, ctx, filename, line_number)
+    parts = _re_subst.split(fmt)
+    for i in range(len(parts)):
+      piece = parts[i]
+      if i%2 == 1 and piece != '%':
+        idx = int(piece)
+        if idx < len(args):
+          piece = _get_value(args[idx], ctx, filename, line_number)
+        else:
+          piece = '<undef>'
+      for t in transforms:
+        piece = t(piece)
+      fp.write(piece)
+
+  def _cmd_include(self, (valref, reader, printer), fp, ctx, filename,
+                   line_number):
+    fname = _get_value(valref, ctx, filename, line_number)
+    ### note: we don't have the set of for_names to pass into this parse.
+    ### I don't think there is anything to do but document it
+    self._execute(self._parse(reader.read_other(fname), base_printer=printer),
+                  fp, ctx)
+
+  def _cmd_insertfile(self, (valref, reader, printer), fp, ctx, filename,
+                      line_number):
+    fname = _get_value(valref, ctx, filename, line_number)
+    fp.write(reader.read_other(fname).text)
+
+  def _cmd_if_any(self, args, fp, ctx, filename, line_number):
+    "If any value is a non-empty string or non-empty list, then T else F."
+    (valrefs, t_section, f_section) = args
+    value = 0
+    for valref in valrefs:
+      if _get_value(valref, ctx, filename, line_number):
+        value = 1
+        break
+    self._do_if(value, t_section, f_section, fp, ctx)
+
+  def _cmd_if_index(self, args, fp, ctx, filename, line_number):
+    ((valref, value), t_section, f_section) = args
+    list, idx = ctx.for_index[valref[0]]
+    if value == 'even':
+      value = idx % 2 == 0
+    elif value == 'odd':
+      value = idx % 2 == 1
+    elif value == 'first':
+      value = idx == 0
+    elif value == 'last':
+      value = idx == len(list)-1
+    else:
+      value = idx == int(value)
+    self._do_if(value, t_section, f_section, fp, ctx)
+
+  def _cmd_is(self, args, fp, ctx, filename, line_number):
+    ((left_ref, right_ref), t_section, f_section) = args
+    right_value = _get_value(right_ref, ctx, filename, line_number)
+    left_value = _get_value(left_ref, ctx, filename, line_number)
+    value = left_value.lower() == right_value.lower()
+    self._do_if(value, t_section, f_section, fp, ctx)
+
+  def _do_if(self, value, t_section, f_section, fp, ctx):
+    if t_section is None:
+      t_section = f_section
+      f_section = None
+    if value:
+      section = t_section
+    else:
+      section = f_section
+    if section is not None:
+      self._execute(section, fp, ctx)
+
+  def _cmd_for(self, args, fp, ctx, filename, line_number):
+    ((valref,), unused, section) = args
+    list = _get_value(valref, ctx, filename, line_number)
+    refname = valref[0]
+    if isinstance(list, basestring):
+      raise NeedSequenceError(refname, filename, line_number)
+    ctx.for_index[refname] = idx = [ list, 0 ]
+    for item in list:
+      self._execute(section, fp, ctx)
+      idx[1] = idx[1] + 1
+    del ctx.for_index[refname]
+
+  def _cmd_define(self, args, fp, ctx, filename, line_number):
+    ((name,), unused, section) = args
+    valfp = StringIO.StringIO()
+    if section is not None:
+      self._execute(section, valfp, ctx)
+    ctx.defines[name] = valfp.getvalue()
+
+def boolean(value):
+  "Return a value suitable for [if-any bool_var] usage in a template."
+  if value:
+    return 'yes'
+  return None
+
+
+def _prepare_ref(refname, for_names, file_args):
+  """refname -> a string containing a dotted identifier. example:"foo.bar.bang"
+  for_names -> a list of active for sequences.
+
+  Returns a `value reference', a 3-tuple made out of (refname, start, rest),
+  for fast access later.
+  """
+  # is the reference a string constant?
+  if refname[0] == '"':
+    return None, refname[1:-1], None
+
+  parts = refname.split('.')
+  start = parts[0]
+  rest = parts[1:]
+
+  # if this is an include-argument, then just return the prepared ref
+  if start[:3] == 'arg':
+    try:
+      idx = int(start[3:])
+    except ValueError:
+      pass
+    else:
+      if idx < len(file_args):
+        orig_refname, start, more_rest = file_args[idx]
+        if more_rest is None:
+          # the include-argument was a string constant
+          return None, start, None
+
+        # prepend the argument's "rest" for our further processing
+        rest[:0] = more_rest
+
+        # rewrite the refname to ensure that any potential 'for' processing
+        # has the correct name
+        ### this can make it hard for debugging include files since we lose
+        ### the 'argNNN' names
+        if not rest:
+          return start, start, [ ]
+        refname = start + '.' + '.'.join(rest)
+
+  if for_names:
+    # From last to first part, check if this reference is part of a for loop
+    for i in range(len(parts), 0, -1):
+      name = '.'.join(parts[:i])
+      if name in for_names:
+        return refname, name, parts[i:]
+
+  return refname, start, rest
+
+def _get_value((refname, start, rest), ctx, filename, line_number):
+  """(refname, start, rest) -> a prepared `value reference' (see above).
+  ctx -> an execution context instance.
+
+  Does a name space lookup within the template name space.  Active
+  for blocks take precedence over data dictionary members with the
+  same name.
+  """
+  if rest is None:
+    # it was a string constant
+    return start
+
+  # get the starting object
+  if ctx.for_index.has_key(start):
+    list, idx = ctx.for_index[start]
+    ob = list[idx]
+  elif ctx.defines.has_key(start):
+    ob = ctx.defines[start]
+  elif hasattr(ctx.data, start):
+    ob = getattr(ctx.data, start)
+  else:
+    raise UnknownReference(refname, filename, line_number)
+
+  # walk the rest of the dotted reference
+  for attr in rest:
+    try:
+      ob = getattr(ob, attr)
+    except AttributeError:
+      raise UnknownReference(refname, filename, line_number)
+
+  # make sure we return a string instead of some various Python types
+  if isinstance(ob, (IntType, FloatType, LongType)):
+    return str(ob)
+  if ob is None:
+    return ''
+
+  # string or a sequence
+  return ob
+
+def _replace(s, replace_map):
+  for orig, repl in replace_map:
+    s = s.replace(orig, repl)
+  return s
+
+REPLACE_JS_MAP = (
+  ('\\', r'\\'), ('\t', r'\t'), ('\n', r'\n'), ('\r', r'\r'),
+  ('"', r'\x22'), ('\'', r'\x27'), ('&', r'\x26'),
+  ('<', r'\x3c'), ('>', r'\x3e'), ('=', r'\x3d'),
+)
+
+# Various unicode whitespace
+REPLACE_JS_UNICODE_MAP = (
+  (u'\u0085', r'\u0085'), (u'\u2028', r'\u2028'), (u'\u2029', r'\u2029'),
+)
+
+# Why not cgi.escape? It doesn't do single quotes which are occasionally
+# used to contain HTML attributes and event handler definitions (unfortunately)
+REPLACE_HTML_MAP = (
+  ('&', '&amp;'), ('<', '&lt;'), ('>', '&gt;'),
+  ('"', '&quot;'), ('\'', '&#39;'),
+)
+
+def _js_escape(s):
+  s = _replace(s, REPLACE_JS_MAP)
+  ### perhaps attempt to coerce the string to unicode and then replace?
+  if isinstance(s, unicode):
+    s = _replace(s, REPLACE_JS_UNICODE_MAP)
+  return s
+
+def _html_escape(s):
+  return _replace(s, REPLACE_HTML_MAP)
+
+def _url_escape(s):
+  ### quote_plus barfs on non-ASCII characters. According to
+  ### http://www.w3.org/International/O-URL-code.html URIs should be
+  ### UTF-8 encoded first.
+  if isinstance(s, unicode):
+    s = s.encode('utf8')
+  return urllib.quote_plus(s)
+
+FORMATTERS = {
+  FORMAT_RAW: None,
+  FORMAT_HTML: _html_escape,
+  FORMAT_XML: _html_escape,   ### use the same quoting as HTML for now
+  FORMAT_JS: _js_escape,
+  FORMAT_URL: _url_escape,
+}
+
+def _parse_format(format_string=FORMAT_RAW):
+  format_funcs = []
+  try:
+    for fspec in format_string.split(','):
+      format_func = FORMATTERS[fspec]
+      if format_func is not None:
+        format_funcs.append(format_func)
+  except KeyError:
+    raise UnknownFormatConstantError(format_string)
+  return format_funcs
+
+class _context:
+  """A container for the execution context"""
+
+
+class Reader:
+  """Abstract class which allows EZT to detect Reader objects."""
+  def filename(self):
+    return '(%s does not provide filename() method)' % repr(self)
+
+class _FileReader(Reader):
+  """Reads templates from the filesystem."""
+  def __init__(self, fname):
+    self.text = open(fname, 'rb').read()
+    self._dir = os.path.dirname(fname)
+    self.fname = fname
+  def read_other(self, relative):
+    return _FileReader(os.path.join(self._dir, relative))
+  def filename(self):
+    return self.fname
+
+class _TextReader(Reader):
+  """'Reads' a template from provided text."""
+  def __init__(self, text):
+    self.text = text
+  def read_other(self, relative):
+    raise BaseUnavailableError()
+  def filename(self):
+    return '(text)'
+
+
+class EZTException(Exception):
+  """Parent class of all EZT exceptions."""
+  def __init__(self, message=None, filename=None, line_number=None):
+    self.message = message
+    self.filename = filename
+    self.line_number = line_number
+  def __str__(self):
+    ret = []
+    if self.message is not None:
+      ret.append(self.message)
+    if self.filename is not None:
+      ret.append('in file ' + str(self.filename))
+    if self.line_number is not None:
+      ret.append('at line ' + str(self.line_number))
+    return ' '.join(ret)
+
+class ArgCountSyntaxError(EZTException):
+  """A bracket directive got the wrong number of arguments."""
+
+class UnknownReference(EZTException):
+  """The template references an object not contained in the data dictionary."""
+
+class NeedSequenceError(EZTException):
+  """The object dereferenced by the template is no sequence (tuple or list)."""
+
+class UnclosedBlocksError(EZTException):
+  """This error may be simply a missing [end]."""
+
+class UnmatchedEndError(EZTException):
+  """This error may be caused by a misspelled if directive."""
+
+class BaseUnavailableError(EZTException):
+  """Base location is unavailable, which disables includes."""
+
+class BadFormatConstantError(EZTException):
+  """Format specifiers must be string constants."""
+
+class UnknownFormatConstantError(EZTException):
+  """The format specifier is an unknown value."""
+
+
+# --- standard test environment ---
+def test_parse():
+  assert _re_parse.split('[a]') == ['', '[a]', None, '']
+  assert _re_parse.split('[a] [b]') == \
+         ['', '[a]', None, ' ', '[b]', None, '']
+  assert _re_parse.split('[a c] [b]') == \
+         ['', '[a c]', None, ' ', '[b]', None, '']
+  assert _re_parse.split('x [a] y [b] z') == \
+         ['x ', '[a]', None, ' y ', '[b]', None, ' z']
+  assert _re_parse.split('[a "b" c "d"]') == \
+         ['', '[a "b" c "d"]', None, '']
+  assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \
+         ['', '["a \\"b[foo]" c.d f]', None, '']
+
+def _test(argv):
+  import doctest, ezt
+  verbose = "-v" in argv
+  return doctest.testmod(ezt, verbose=verbose)
+
+if __name__ == "__main__":
+  # invoke unit test for this module:
+  import sys
+  sys.exit(_test(sys.argv)[0])

Propchange: steve/trunk/lib/ezt.py
------------------------------------------------------------------------------
    svn:eol-style = native


Reply via email to