changeset 609:237050080827

Add support for Python code blocks in text templates using the new syntax.
author cmlenz
date Mon, 27 Aug 2007 22:39:01 +0000
parents 3ce3f8477996
children 6a37018199fd
files ChangeLog doc/templates.txt genshi/template/base.py genshi/template/markup.py genshi/template/tests/text.py genshi/template/text.py
diffstat 6 files changed, 111 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -18,10 +18,10 @@
    templates are basically inlined into the including template, which can
    speed up rendering of that template a bit.
  * Added new syntax for text templates, which is more powerful and flexible
-   with respect to white-space and line breaks. The old syntax is still
-   available and the default for now, but in a future release the new syntax
-   will become the default, and some time affter that the old syntax will be
-   removed.
+   with respect to white-space and line breaks. It also supports Python code
+   blocks. The old syntax is still available and the default for now, but in a
+   future release the new syntax will become the default, and some time after
+   that the old syntax will be removed.
  * Added support for passing optimization hints to `<py:match>` directives,
    which can speed up match templates in many cases, for example when a match
    template should only be applied once to a stream, or when it should not be
--- a/doc/templates.txt
+++ b/doc/templates.txt
@@ -208,8 +208,8 @@
 Code Blocks
 ===========
 
-XML templates also support full Python code blocks using the ``<?python ?>``
-processing instruction:
+Templates also support full Python code blocks, using the ``<?python ?>``
+processing instruction in XML templates:
 
 .. code-block:: genshi
 
@@ -223,12 +223,29 @@
 
 This will produce the following output:
 
-.. code-block:: genshi
+.. code-block:: xml
 
   <div>
     <b>Hello, world!</b>
   </div>
 
+In text templates (although only those using the new syntax introduced in
+Genshi 0.5), code blocks use the special ``{% python %}`` directive:
+
+.. code-block:: genshitext
+
+  {% python
+      from genshi.builder import tag
+      def greeting(name):
+          return tag.b('Hello, %s!' % name')
+  %}
+  ${greeting('world')}
+
+This will produce the following output::
+
+  Hello, world!
+
+
 Code blocks can import modules, define classes and functions, and basically do
 anything you can do in normal Python code. What code blocks can *not* do is to
 produce content that is emitted directly tp the generated output.
@@ -253,8 +270,6 @@
 into a sandboxable template engine; there are sufficient ways to do harm even
 using plain expressions.
 
-.. note:: Code blocks are not currently supported in text templates.
-
 
 .. _`error handling`:
 
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -21,6 +21,7 @@
         def popleft(self): return self.pop(0)
 import os
 from StringIO import StringIO
+import sys
 
 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
 from genshi.input import ParseError
@@ -29,6 +30,11 @@
            'TemplateSyntaxError', 'BadDirectiveError']
 __docformat__ = 'restructuredtext en'
 
+if sys.version_info < (2, 4):
+    _ctxt2dict = lambda ctxt: ctxt.frames[0]
+else:
+    _ctxt2dict = lambda ctxt: ctxt
+
 
 class TemplateError(Exception):
     """Base exception class for errors related to template processing."""
@@ -278,6 +284,9 @@
     """
     __metaclass__ = TemplateMeta
 
+    EXEC = StreamEventKind('EXEC')
+    """Stream event kind representing a Python code suite to execute."""
+
     EXPR = StreamEventKind('EXPR')
     """Stream event kind representing a Python expression."""
 
@@ -332,7 +341,7 @@
             self.stream = list(self._prepare(self._parse(source, encoding)))
         except ParseError, e:
             raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
-        self.filters = [self._flatten, self._eval]
+        self.filters = [self._flatten, self._eval, self._exec]
         if loader:
             self.filters.append(self._include)
 
@@ -475,6 +484,14 @@
             else:
                 yield kind, data, pos
 
+    def _exec(self, stream, ctxt):
+        """Internal stream filter that executes Python code blocks."""
+        for event in stream:
+            if event[0] is EXEC:
+                event[1].execute(_ctxt2dict(ctxt))
+            else:
+                yield event
+
     def _flatten(self, stream, ctxt):
         """Internal stream filter that expands `SUB` events in the stream."""
         for event in stream:
@@ -519,6 +536,7 @@
                 yield event
 
 
+EXEC = Template.EXEC
 EXPR = Template.EXPR
 INCLUDE = Template.INCLUDE
 SUB = Template.SUB
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -14,23 +14,17 @@
 """Markup templating engine."""
 
 from itertools import chain
-import sys
 
 from genshi.core import Attrs, Namespace, Stream, StreamEventKind
 from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
 from genshi.input import XMLParser
 from genshi.template.base import BadDirectiveError, Template, \
                                  TemplateSyntaxError, _apply_directives, \
-                                 INCLUDE, SUB
+                                 EXEC, INCLUDE, SUB
 from genshi.template.eval import Suite
 from genshi.template.interpolation import interpolate
 from genshi.template.directives import *
 
-if sys.version_info < (2, 4):
-    _ctxt2dict = lambda ctxt: ctxt.frames[0]
-else:
-    _ctxt2dict = lambda ctxt: ctxt
-
 __all__ = ['MarkupTemplate']
 __docformat__ = 'restructuredtext en'
 
@@ -46,8 +40,6 @@
       <li>1</li><li>2</li><li>3</li>
     </ul>
     """
-    EXEC = StreamEventKind('EXEC')
-    """Stream event kind representing a Python code suite to execute."""
 
     DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
     XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
@@ -74,7 +66,7 @@
         # Make sure the include filter comes after the match filter
         if loader:
             self.filters.remove(self._include)
-        self.filters += [self._exec, self._match]
+        self.filters += [self._match]
         if loader:
             self.filters.append(self._include)
 
@@ -221,16 +213,6 @@
         assert len(streams) == 1
         return streams[0]
 
-    def _exec(self, stream, ctxt):
-        """Internal stream filter that executes code in ``<?python ?>``
-        processing instructions.
-        """
-        for event in stream:
-            if event[0] is EXEC:
-                event[1].execute(_ctxt2dict(ctxt))
-            else:
-                yield event
-
     def _match(self, stream, ctxt, match_templates=None):
         """Internal stream filter that applies any defined match templates
         to the stream.
@@ -308,6 +290,3 @@
 
             else: # no matches
                 yield event
-
-
-EXEC = MarkupTemplate.EXEC
--- a/genshi/template/tests/text.py
+++ b/genshi/template/tests/text.py
@@ -180,6 +180,36 @@
 
 """, tmpl.generate(items=range(3)).render())
 
+    def test_exec_with_trailing_space(self):
+        """
+        Verify that a code block with trailing space does not cause a syntax
+        error (see ticket #127).
+        """
+        NewTextTemplate(u"""
+          {% python
+            bar = 42
+          $}
+        """)
+
+    def test_exec_import(self):
+        tmpl = NewTextTemplate(u"""{% python from datetime import timedelta %}
+        ${timedelta(days=2)}
+        """)
+        self.assertEqual("""
+        2 days, 0:00:00
+        """, str(tmpl.generate()))
+
+    def test_exec_def(self):
+        tmpl = NewTextTemplate(u"""{% python
+        def foo():
+            return 42
+        %}
+        ${foo()}
+        """)
+        self.assertEqual(u"""
+        42
+        """, str(tmpl.generate()))
+
     def test_include(self):
         file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
         try:
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -28,7 +28,8 @@
 
 import re
 
-from genshi.template.base import BadDirectiveError, Template, INCLUDE, SUB
+from genshi.template.base import BadDirectiveError, Template, EXEC, INCLUDE, SUB
+from genshi.template.eval import Suite
 from genshi.template.directives import *
 from genshi.template.directives import Directive, _apply_directives
 from genshi.template.interpolation import interpolate
@@ -142,7 +143,7 @@
         self._delims = delims
         self._directive_re = re.compile(self._DIRECTIVE_RE % tuple(
             map(re.escape, delims)
-        ))
+        ), re.DOTALL)
         self._escape_re = re.compile(self._ESCAPE_RE % tuple(
             map(re.escape, delims[::2])
         ))
@@ -183,24 +184,39 @@
 
             lineno += len(source[start:end].splitlines())
             command, value = mo.group(2, 3)
-            if command:
-                if command == 'include':
-                    pos = (self.filename, lineno, 0)
-                    stream.append((INCLUDE, (value.strip(), []), pos))
-                elif command == 'end':
-                    depth -= 1
-                    if depth in dirmap:
-                        directive, start_offset = dirmap.pop(depth)
-                        substream = stream[start_offset:]
-                        stream[start_offset:] = [(SUB, ([directive], substream),
-                                                  (self.filepath, lineno, 0))]
-                else:
-                    cls = self._dir_by_name.get(command)
-                    if cls is None:
-                        raise BadDirectiveError(command)
-                    directive = cls, value, None, (self.filepath, lineno, 0)
-                    dirmap[depth] = (directive, len(stream))
-                    depth += 1
+
+            if command == 'include':
+                pos = (self.filename, lineno, 0)
+                stream.append((INCLUDE, (value.strip(), []), pos))
+
+            elif command == 'python':
+                if not self.allow_exec:
+                    raise TemplateSyntaxError('Python code blocks not allowed',
+                                              self.filepath, lineno)
+                try:
+                    suite = Suite(value, self.filepath, lineno,
+                                  lookup=self.lookup)
+                except SyntaxError, err:
+                    raise TemplateSyntaxError(err, self.filepath,
+                                              lineno + (err.lineno or 1) - 1)
+                pos = (self.filename, lineno, 0)
+                stream.append((EXEC, suite, pos))
+
+            elif command == 'end':
+                depth -= 1
+                if depth in dirmap:
+                    directive, start_offset = dirmap.pop(depth)
+                    substream = stream[start_offset:]
+                    stream[start_offset:] = [(SUB, ([directive], substream),
+                                              (self.filepath, lineno, 0))]
+
+            elif command:
+                cls = self._dir_by_name.get(command)
+                if cls is None:
+                    raise BadDirectiveError(command)
+                directive = cls, value, None, (self.filepath, lineno, 0)
+                dirmap[depth] = (directive, len(stream))
+                depth += 1
 
             offset = end
 
Copyright (C) 2012-2017 Edgewall Software