SVN: r25659 - in trunk/quixote: . test

David Binger <dbinger-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Mon, 22 Nov 2004 11:32:28 -0500
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: dbinger
Date: 2004-11-22 11:29:27 -0500 (Mon, 22 Nov 2004)
New Revision: 25659

Modified:
   trunk/quixote/ptl_compile.py
   trunk/quixote/test/utest_ptl.py
Log:
Support $-substitution in templates, as in python 2.4's string.Template class.
$-substitution is applied to every '$'-containing literal in a template.
Templates written before this change, if they contain literals containing '$', 
*must* be converted by replacing each '$' with '$$'.

Drop support for old-style ("template f()") template definitions.


Modified: trunk/quixote/ptl_compile.py
===================================================================
--- trunk/quixote/ptl_compile.py	2004-11-22 14:16:20 UTC (rev 25658)
+++ trunk/quixote/ptl_compile.py	2004-11-22 16:29:27 UTC (rev 25659)
@@ -1,8 +1,8 @@
 #!/www/python/bin/python
-#$HeadURL$
-#$Id$
+"""
+$URL$
+$Id$
 
-"""
 Compile a PTL template.
 
 First template function names are mangled, noting the template type.
@@ -30,18 +30,8 @@
 from compiler.consts import OP_ASSIGN
 from compiler import misc, syntax
 
-
-# magic names inserted into the code
-IO_MODULE = "quixote.html"
-IO_CLASS = "TemplateIO"
-IO_MANGLED_CLASS = "_q_TemplateIO"
-IO_INSTANCE = "_q_output"
 HTML_TEMPLATE_PREFIX = "_q_html_template_"
 PLAIN_TEMPLATE_PREFIX = "_q_plain_template_"
-TEMPLATE_PREFIX = "_q_template_"
-MARKUP_MODULE = "quixote.html"
-MARKUP_CLASS = "htmltext"
-MARKUP_MANGLED_CLASS = "_q_htmltext"
 
 class TemplateTransformer(transformer.Transformer):
 
@@ -49,23 +39,22 @@
         transformer.Transformer.__init__(self, *args, **kwargs)
         self.__template_type = [] # stack, "html", "plain" or None
 
+    def _get_template_type(self):
+        """Return the type of the function being compiled ("html", "plain"
+        or None)."""
+        if self.__template_type:
+            return self.__template_type[-1]
+        else:
+            return None
+
     def file_input(self, nodelist):
-        # Add a "from IO_MODULE import IO_CLASS" statement to the
-        # beginning of the module.
         doc = None # self.get_docstring(nodelist, symbol.file_input)
 
-        io_imp = ast.From(IO_MODULE, [(IO_CLASS, IO_MANGLED_CLASS)])
-        markup_imp = ast.From(MARKUP_MODULE,
-                              [(MARKUP_CLASS, MARKUP_MANGLED_CLASS)])
+        html_imp = ast.From('quixote.html', [('TemplateIO', '_q_TemplateIO'),
+                                             ('htmltext', '_q_htmltext')])
+        vars_imp = ast.From("__builtin__", [("vars", "_q_vars")])
+        stmts = [ vars_imp, html_imp ]
 
-        # Add an IO_INSTANCE binding for module level expressions (like
-        # doc strings).  This instance will not be returned.
-        io_instance = ast.CallFunc(ast.Name(IO_MANGLED_CLASS), [])
-        io_assign_name = ast.AssName(IO_INSTANCE, OP_ASSIGN)
-        io_assign = ast.Assign([io_assign_name], io_instance)
-
-        stmts = [ io_imp, io_assign, markup_imp ]
-
         for node in nodelist:
             if node[0] != token.ENDMARKER and node[0] != token.NEWLINE:
                 self.com_append_stmt(stmts, node)
@@ -96,16 +85,11 @@
             elif name.startswith(HTML_TEMPLATE_PREFIX):
                 name = name[len(HTML_TEMPLATE_PREFIX):]
                 template_type = "html"
-            elif name.startswith(TEMPLATE_PREFIX):
-                name = name[len(TEMPLATE_PREFIX):]
-                template_type = "plain"
             else:
                 raise RuntimeError, 'unknown prefix on %s' % name
 
             self.__template_type.append(template_type)
 
-            # Add "IO_INSTANCE = IO_CLASS()" statement at the beginning of
-            # the function and a "return IO_INSTANCE" at the end.
             if args[0] == symbol.varargslist:
                 names, defaults, flags = self.com_arglist(args[1:])
             else:
@@ -116,15 +100,15 @@
             # code for function
             code = self.com_node(nodelist[-1])
 
-            # create an instance, assign to IO_INSTANCE
-            klass = ast.Name(IO_MANGLED_CLASS)
+            # _q_output = _q_TemplateIO()
+            klass = ast.Name('_q_TemplateIO')
             args = [ast.Const(template_type == "html")]
             instance = ast.CallFunc(klass, args)
-            assign_name = ast.AssName(IO_INSTANCE, OP_ASSIGN)
+            assign_name = ast.AssName('_q_output', OP_ASSIGN)
             assign = ast.Assign([assign_name], instance)
 
-            # return the IO_INSTANCE.getvalue(...)
-            func = ast.Getattr(ast.Name(IO_INSTANCE), "getvalue")
+            # return _q_output.getvalue()
+            func = ast.Getattr(ast.Name('_q_output'), "getvalue")
             ret = ast.Return(ast.CallFunc(func, []))
 
             # wrap original function code
@@ -141,14 +125,14 @@
         return n
 
     def expr_stmt(self, nodelist):
-        if not self.__template_type or not self.__template_type[-1]:
+        if self._get_template_type() is None:
             return transformer.Transformer.expr_stmt(self, nodelist)
 
         # Instead of discarding objects on the stack, call
-        # "IO_INSTANCE += obj".
+        # "_q_output += obj".
         exprNode = self.com_node(nodelist[-1])
         if len(nodelist) == 1:
-            lval = ast.Name(IO_INSTANCE)
+            lval = ast.Name('_q_output')
             n = ast.AugAssign(lval, '+=', exprNode)
             if hasattr(exprNode, 'lineno'):
                 n.lineno = exprNode.lineno
@@ -169,13 +153,45 @@
         k = ''
         for node in nodelist:
             k = k + eval(node[1])
-        n = ast.Const(k)
-        if self.__template_type and self.__template_type[-1] == "html":
-            # change "foo" to _q_htmltext("foo")
-            n = ast.CallFunc(ast.Name(MARKUP_MANGLED_CLASS), [n])
-        return n
+        lineno = node[2]
+        if self._get_template_type() is not None and '$' in k:
+            try:
+                k = _convert_string(k)
+            except ValueError, e:
+                raise SyntaxError(str(e), (None, lineno, None, None))
+            return ast.Mod((self._get_text_node(k),
+                            ast.CallFunc(ast.Name('_q_vars'), [])))
+        else:
+            return self._get_text_node(k)
 
+    def _get_text_node(self, k):
+        if self._get_template_type() == "html":
+            return ast.CallFunc(ast.Name('_q_htmltext'), [ast.Const(k)])
+        else:
+            return ast.Const(k)
 
+_substitution_pattern = r"""
+    \$(?:
+    (?P<escaped>\$) |
+    (?P<named>%(idpat)s) |
+    {(?P<braced>%(idpat)s)} |
+    (?P<invalid>.*)
+    )""" % dict(idpat='[_a-z][_a-z0-9]*')
+
+_substitution_re = re.compile(_substitution_pattern, re.I|re.VERBOSE)
+
+def _convert_string(s):
+    def convert(mo):
+        name = mo.group('named') or mo.group('braced')
+        if name is not None:
+            return '%(' + name + ')s'
+        elif mo.group('escaped') is not None:
+            return '$'
+        elif mo.group('invalid') is not None:
+            invalid = '$' + mo.group('invalid')
+            raise ValueError('invalid substitution %r' % invalid)
+    return _substitution_re.sub(convert, s.replace('%', '%%'))
+
 _template_re = re.compile(r"^([ \t]*) def (?:[ \t]+)"                # def
                           r" ([a-zA-Z_][a-zA-Z_0-9]*)"               # <name>
                           r" (?:[ \t]*) \[(plain|html)\] (?:[ \t]*)" # <type>
@@ -241,7 +257,7 @@
     return template.code
 
 def compile(inputname, outputname):
-    """compile(inputname, outputname)
+    """(inputname, outputname)
 
     Compile a template file.  The new template is writen to outputname.
     """

Modified: trunk/quixote/test/utest_ptl.py
===================================================================
--- trunk/quixote/test/utest_ptl.py	2004-11-22 14:16:20 UTC (rev 25658)
+++ trunk/quixote/test/utest_ptl.py	2004-11-22 16:29:27 UTC (rev 25659)
@@ -53,6 +53,35 @@
         except SyntaxError, e:
             assert e.lineno == 1
 
+    def check_dollar(self):
+        run_ptl('def f(a):',
+                '    "yes $a"',
+                'assert f(1) == None')
+        run_ptl('def f [plain] (a):',
+                '    "yes $a"',
+                'assert f(1) == "yes 1"')
+        run_ptl('def f [html] (a):',
+                '    "yes ${a} $a $$"',
+                'assert f(1) == "yes 1 1 $"')
+        try:
+            run_ptl('def f [plain] (a):',
+                    '    "yes ${a} $a $"')
+            assert 0
+        except SyntaxError, e:
+            assert str(e) == "invalid substitution '$' (line 2) (test, line 2)"
+        try:
+            run_ptl('def f [plain] (a):',
+                    '    "$-"')
+            assert 0
+        except SyntaxError, e:
+            assert str(e) == (
+                "invalid substitution '$-' (line 2) (test, line 2)")
+        run_ptl('def f [html] (a):',
+                '    "yes %s $a" % "odd"',
+                'assert f(1) == "yes odd 1"')
+        run_ptl('def f [plain] (a):',
+                '    "yes %(a)s $a" % locals()',
+                'assert f(1) == "yes 1 1"')
 
 if __name__ == "__main__":
     Test()