# HG changeset patch # User cmlenz # Date 1188254341 0 # Node ID 6d4877844e2880f2c3c4c911c07fa0c468bd37c4 # Parent 4f196970844254a094bde69a4e9d39918f42849f Add support for Python code blocks in text templates using the new syntax. diff --git a/ChangeLog b/ChangeLog --- 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 `` 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 diff --git a/doc/templates.txt b/doc/templates.txt --- a/doc/templates.txt +++ b/doc/templates.txt @@ -208,8 +208,8 @@ Code Blocks =========== -XML templates also support full Python code blocks using the ```` -processing instruction: +Templates also support full Python code blocks, using the ```` +processing instruction in XML templates: .. code-block:: genshi @@ -223,12 +223,29 @@ This will produce the following output: -.. code-block:: genshi +.. code-block:: xml
Hello, world!
+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`: diff --git a/genshi/template/base.py b/genshi/template/base.py --- 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 diff --git a/genshi/template/markup.py b/genshi/template/markup.py --- 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 @@
  • 1
  • 2
  • 3
  • """ - 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 ```` - 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 diff --git a/genshi/template/tests/text.py b/genshi/template/tests/text.py --- 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: diff --git a/genshi/template/text.py b/genshi/template/text.py --- 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