# HG changeset patch # User cmlenz # Date 1163441817 0 # Node ID 94639584725a8a16d1c7fbff00dc4237fae38d68 # Parent 4a2050e9dcd8590662bec0bcc020bc4c3f248ecf inline branch: Merged [430:434/trunk]. diff --git a/doc/text-templates.txt b/doc/text-templates.txt --- a/doc/text-templates.txt +++ b/doc/text-templates.txt @@ -116,7 +116,8 @@ Template Directives ------------------- -Directives are lines starting with a ``#`` character followed immediately by the directive name. They can affect how the template is rendered in a number of +Directives are lines starting with a ``#`` character followed immediately by +the directive name. They can affect how the template is rendered in a number of ways: Genshi provides directives for conditionals and looping, among others. Directives must be on separate lines, and the ``#`` character must be be the diff --git a/doc/xml-templates.txt b/doc/xml-templates.txt --- a/doc/xml-templates.txt +++ b/doc/xml-templates.txt @@ -104,7 +104,7 @@ You **will** however get a ``NameError`` if you try to call an undefined variable, or do anything else with it, such as accessing its attributes. If you need to know whether a variable is defined, you can check its type against the -``Undefined`` class, for example in an `py:if`_ directive:: +``Undefined`` class, for example in a `py:if`_ directive:: >>> from genshi.template import TextTemplate >>> tmpl = TextTemplate('${type(doh) is Undefined}') diff --git a/genshi/template/core.py b/genshi/template/core.py --- a/genshi/template/core.py +++ b/genshi/template/core.py @@ -178,6 +178,15 @@ expr = ' "%s"' % self.expr.source return '<%s%s>' % (self.__class__.__name__, expr) + def prepare(self, directives, stream): + """Called after the template stream has been completely parsed. + + The part of the template stream associated with the directive will be + replaced by what this function returns. This allows the directive to + optimize the template or validate the way the directive is used. + """ + return stream + def tagname(self): """Return the local tag name of the directive as it is used in templates. @@ -231,7 +240,7 @@ self.filters = [self._flatten, self._eval] - self.stream = self._parse(encoding) + self.stream = list(self._prepare(self._parse(encoding))) def __repr__(self): return '<%s "%s">' % (self.__class__.__name__, self.filename) @@ -289,6 +298,25 @@ return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]) _interpolate = classmethod(_interpolate) + def _prepare(self, stream): + """Call the `prepare` method of every directive instance in the + template so that various optimization and validation tasks can be + performed. + """ + for kind, data, pos in stream: + if kind is SUB: + directives, substream = data + for directive in directives[:]: + substream = directive.prepare(directives, substream) + substream = self._prepare(substream) + if directives: + yield kind, (directives, list(substream)), pos + else: + for event in substream: + yield event + else: + yield kind, data, pos + def compile(self): """Compile the template to a Python module, and return the module object. diff --git a/genshi/template/directives.py b/genshi/template/directives.py --- a/genshi/template/directives.py +++ b/genshi/template/directives.py @@ -114,16 +114,9 @@ """ __slots__ = [] - def __call__(self, stream, ctxt, directives): - def _generate(): - yield stream.next() - yield EXPR, self.expr, (None, -1, -1) - event = stream.next() - for next in stream: - event = next - yield event - - return _apply_directives(_generate(), ctxt, directives) + def prepare(self, directives, stream): + directives.remove(self) + return [stream[0], (EXPR, self.expr, (None, -1, --1)), stream[-1]] class DefDirective(Directive): @@ -368,8 +361,9 @@ """ __slots__ = [] - def __call__(self, stream, ctxt, directives): - yield EXPR, self.expr, (None, -1, -1) + def prepare(self, directives, stream): + directives.remove(self) + return [(EXPR, self.expr, (None, -1, -1))] class StripDirective(Directive): @@ -407,11 +401,7 @@ def __call__(self, stream, ctxt, directives): def _generate(): - if self.expr: - strip = self.expr.evaluate(ctxt) - else: - strip = True - if strip: + if self.expr.evaluate(ctxt): stream.next() # skip start tag previous = stream.next() for event in stream: @@ -420,8 +410,13 @@ else: for event in stream: yield event + return _apply_directives(_generate(), ctxt, directives) - return _apply_directives(_generate(), ctxt, directives) + def prepare(self, directives, stream): + if not self.expr: + directives.remove(self) + return stream[1:-1] + return stream class ChooseDirective(Directive): diff --git a/genshi/template/inline.py b/genshi/template/inline.py --- a/genshi/template/inline.py +++ b/genshi/template/inline.py @@ -174,23 +174,7 @@ yield w() yield w('# Applying %r', directive) - if isinstance(directive, ContentDirective): - ei[0] += 1 - yield w('for e in _expand(E%d.evaluate(ctxt), %r):', - p_exprs[directive.expr], (None, -1, -1)) - w.shift() - lines = _apply(directives, stream) - for line in lines: - yield line - break - yield w('yield e') - line = lines.next() - for next in lines: - line = next - yield line - w.unshift() - - elif isinstance(directive, DefDirective): + if isinstance(directive, DefDirective): pass elif isinstance(directive, ForDirective): @@ -211,12 +195,6 @@ yield line w.unshift() - elif isinstance(directive, ReplaceDirective): - ei[0] += 1 - yield w('for e in _expand(E%d.evaluate(ctxt), %r): yield e', - p_exprs[directive.expr], - (None, -1, -1)) - elif isinstance(directive, WithDirective): for targets, expr in directive.vars: ei[0] += 1 @@ -228,28 +206,19 @@ yield w('ctxt.pop()') elif isinstance(directive, StripDirective): - if directive.expr: - yield w('if E%d.evaluate(ctxt):', p_exprs[directive.expr]) - w.shift() - lines = _apply(directives, stream) - previous = lines.next() - for line in lines: - yield previous - previous = line - w.unshift() - yield w('else:') - w.shift() - for line in _apply(directives, stream): - yield line - w.unshift() - else: # always strip - lines = _apply(directives, stream) - yield w('# stripped %r', lines.next().strip()) - previous = lines.next() - for line in lines: - yield previous - previous = line - yield w('# stripped %r', previous.strip()) + yield w('if E%d.evaluate(ctxt):', p_exprs[directive.expr]) + w.shift() + lines = _apply(directives, stream) + previous = lines.next() + for line in lines: + yield previous + previous = line + w.unshift() + yield w('else:') + w.shift() + for line in _apply(directives, stream): + yield line + w.unshift() else: raise NotImplementedError diff --git a/genshi/template/plugin.py b/genshi/template/plugin.py --- a/genshi/template/plugin.py +++ b/genshi/template/plugin.py @@ -18,14 +18,17 @@ from pkg_resources import resource_filename -from genshi.eval import Undefined from genshi.input import ET, HTML, XML from genshi.output import DocType from genshi.template.core import Context, Template +from genshi.template.eval import Undefined from genshi.template.loader import TemplateLoader from genshi.template.markup import MarkupTemplate from genshi.template.text import TextTemplate +__all__ = ['ConfigurationError', 'MarkupTemplateEnginePlugin', + 'TextTemplateEnginePlugin'] + class ConfigurationError(Exception): """Exception raised when invalid plugin options are encountered.""" @@ -44,14 +47,15 @@ self.options = options self.default_encoding = options.get('genshi.default_encoding', 'utf-8') - auto_reload = options.get('genshi.auto_reload', '1').lower() \ - in ('1', 'yes', 'true') + auto_reload = options.get('genshi.auto_reload', '1') + if isinstance(auto_reload, basestring): + auto_reload = auto_reload.lower() in ('1', 'on', 'yes', 'true') search_path = options.get('genshi.search_path', '').split(':') try: max_cache_size = int(options.get('genshi.max_cache_size', 25)) except ValueError: raise ConfigurationError('Invalid value for max_cache_size: "%s"' % - max_cache_size) + options.get('genshi.max_cache_size')) self.loader = TemplateLoader(filter(None, search_path), auto_reload=auto_reload, @@ -116,12 +120,12 @@ def __init__(self, extra_vars_func=None, options=None): AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options) - doctype = options.get('genshi.default_doctype') + doctype = self.options.get('genshi.default_doctype') if doctype and doctype not in self.doctypes: raise ConfigurationError('Unknown doctype "%s"' % doctype) self.default_doctype = self.doctypes.get(doctype) - format = options.get('genshi.default_format', 'html') + format = self.options.get('genshi.default_format', 'html') if format not in ('html', 'xhtml', 'xml', 'text'): raise ConfigurationError('Unknown output format "%s"' % format) self.default_format = format @@ -148,11 +152,3 @@ template_class = TextTemplate extension = '.txt' default_format = 'text' - - def transform(self, info, template): - """Render the output to an event stream.""" - data = {} - if self.get_extra_vars: - data.update(self.get_extra_vars()) - data.update(info) - return super(TextTemplateEnginePlugin, self).transform(data, template) diff --git a/genshi/template/tests/__init__.py b/genshi/template/tests/__init__.py --- a/genshi/template/tests/__init__.py +++ b/genshi/template/tests/__init__.py @@ -17,13 +17,14 @@ def suite(): from genshi.template.tests import core, directives, eval, loader, markup, \ - text + plugin, text suite = unittest.TestSuite() suite.addTest(core.suite()) suite.addTest(directives.suite()) suite.addTest(eval.suite()) suite.addTest(loader.suite()) suite.addTest(markup.suite()) + suite.addTest(plugin.suite()) suite.addTest(text.suite()) return suite diff --git a/genshi/template/tests/plugin.py b/genshi/template/tests/plugin.py new file mode 100644 --- /dev/null +++ b/genshi/template/tests/plugin.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006 Matthew Good +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +import doctest +import os +import unittest + +from genshi.core import Stream +from genshi.output import DocType +from genshi.template import MarkupTemplate, TextTemplate +from genshi.template.plugin import ConfigurationError, \ + MarkupTemplateEnginePlugin, \ + TextTemplateEnginePlugin + +PACKAGE = 'genshi.template.tests' + + +class MarkupTemplateEnginePluginTestCase(unittest.TestCase): + + def test_init_no_options(self): + plugin = MarkupTemplateEnginePlugin() + self.assertEqual('utf-8', plugin.default_encoding) + self.assertEqual('html', plugin.default_format) + self.assertEqual(None, plugin.default_doctype) + + self.assertEqual([], plugin.loader.search_path) + self.assertEqual(True, plugin.loader.auto_reload) + self.assertEqual(25, plugin.loader._cache.capacity) + + def test_init_with_loader_options(self): + plugin = MarkupTemplateEnginePlugin(options={ + 'genshi.auto_reload': 'off', + 'genshi.max_cache_size': '100', + 'genshi.search_path': '/usr/share/tmpl:/usr/local/share/tmpl', + }) + self.assertEqual(['/usr/share/tmpl', '/usr/local/share/tmpl'], + plugin.loader.search_path) + self.assertEqual(False, plugin.loader.auto_reload) + self.assertEqual(100, plugin.loader._cache.capacity) + + def test_init_with_invalid_cache_size(self): + self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin, + options={'genshi.max_cache_size': 'thirty'}) + + def test_init_with_output_options(self): + plugin = MarkupTemplateEnginePlugin(options={ + 'genshi.default_encoding': 'iso-8859-15', + 'genshi.default_format': 'xhtml', + 'genshi.default_doctype': 'xhtml-strict', + }) + self.assertEqual('iso-8859-15', plugin.default_encoding) + self.assertEqual('xhtml', plugin.default_format) + self.assertEqual(DocType.XHTML, plugin.default_doctype) + + def test_init_with_invalid_output_format(self): + self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin, + options={'genshi.default_format': 'foobar'}) + + def test_init_with_invalid_doctype(self): + self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin, + options={'genshi.default_doctype': 'foobar'}) + + def test_load_template_from_file(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + self.assertEqual('test.html', os.path.basename(tmpl.filename)) + assert isinstance(tmpl, MarkupTemplate) + + def test_load_template_from_string(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(None, template_string="""

+ $message +

""") + self.assertEqual(None, tmpl.filename) + assert isinstance(tmpl, MarkupTemplate) + + def test_transform_with_load(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + stream = plugin.transform({'message': 'Hello'}, tmpl) + assert isinstance(stream, Stream) + + def test_transform_without_load(self): + plugin = MarkupTemplateEnginePlugin() + stream = plugin.transform({'message': 'Hello'}, + PACKAGE + '.templates.test') + assert isinstance(stream, Stream) + + def test_render(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + output = plugin.render({'message': 'Hello'}, template=tmpl) + self.assertEqual(""" + + + Test + + +

Test

+

Hello

+ +""", output) + + def test_render_with_format(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + output = plugin.render({'message': 'Hello'}, format='xhtml', + template=tmpl) + self.assertEqual(""" + + + Test + + +

Test

+

Hello

+ +""", output) + + def test_render_with_doctype(self): + plugin = MarkupTemplateEnginePlugin(options={ + 'genshi.default_doctype': 'html-strict', + }) + tmpl = plugin.load_template(PACKAGE + '.templates.test') + output = plugin.render({'message': 'Hello'}, template=tmpl) + self.assertEqual(""" + + + Test + + +

Test

+

Hello

+ +""", output) + + def test_helper_functions(self): + plugin = MarkupTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.functions') + output = plugin.render({'snippet': 'Foo'}, template=tmpl) + self.assertEqual("""
+False +bar +Foo +Foo +
""", output) + + +class TextTemplateEnginePluginTestCase(unittest.TestCase): + + def test_init_no_options(self): + plugin = TextTemplateEnginePlugin() + self.assertEqual('utf-8', plugin.default_encoding) + self.assertEqual('text', plugin.default_format) + + self.assertEqual([], plugin.loader.search_path) + self.assertEqual(True, plugin.loader.auto_reload) + self.assertEqual(25, plugin.loader._cache.capacity) + + def test_init_with_loader_options(self): + plugin = TextTemplateEnginePlugin(options={ + 'genshi.auto_reload': 'off', + 'genshi.max_cache_size': '100', + 'genshi.search_path': '/usr/share/tmpl:/usr/local/share/tmpl', + }) + self.assertEqual(['/usr/share/tmpl', '/usr/local/share/tmpl'], + plugin.loader.search_path) + self.assertEqual(False, plugin.loader.auto_reload) + self.assertEqual(100, plugin.loader._cache.capacity) + + def test_init_with_output_options(self): + plugin = TextTemplateEnginePlugin(options={ + 'genshi.default_encoding': 'iso-8859-15', + }) + self.assertEqual('iso-8859-15', plugin.default_encoding) + + def test_load_template_from_file(self): + plugin = TextTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + assert isinstance(tmpl, TextTemplate) + self.assertEqual('test.txt', os.path.basename(tmpl.filename)) + + def test_load_template_from_string(self): + plugin = TextTemplateEnginePlugin() + tmpl = plugin.load_template(None, template_string="$message") + self.assertEqual(None, tmpl.filename) + assert isinstance(tmpl, TextTemplate) + + def test_transform_without_load(self): + plugin = TextTemplateEnginePlugin() + stream = plugin.transform({'message': 'Hello'}, + PACKAGE + '.templates.test') + assert isinstance(stream, Stream) + + def test_transform_with_load(self): + plugin = TextTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + stream = plugin.transform({'message': 'Hello'}, tmpl) + assert isinstance(stream, Stream) + + def test_render(self): + plugin = TextTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.test') + output = plugin.render({'message': 'Hello'}, template=tmpl) + self.assertEqual("""Test +==== + +Hello +""", output) + + def test_helper_functions(self): + plugin = TextTemplateEnginePlugin() + tmpl = plugin.load_template(PACKAGE + '.templates.functions') + output = plugin.render({}, template=tmpl) + self.assertEqual("""False +bar +""", output) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(MarkupTemplateEnginePluginTestCase, 'test')) + suite.addTest(unittest.makeSuite(TextTemplateEnginePluginTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/genshi/template/tests/templates/__init__.py b/genshi/template/tests/templates/__init__.py new file mode 100644 diff --git a/genshi/template/tests/templates/functions.html b/genshi/template/tests/templates/functions.html new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/functions.html @@ -0,0 +1,6 @@ +
+${defined('foo')} +${value_of('foo', 'bar')} +${XML(snippet)} +${HTML(snippet)} +
diff --git a/genshi/template/tests/templates/functions.txt b/genshi/template/tests/templates/functions.txt new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/functions.txt @@ -0,0 +1,2 @@ +${defined('foo')} +${value_of('foo', 'bar')} diff --git a/genshi/template/tests/templates/test.html b/genshi/template/tests/templates/test.html new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/test.html @@ -0,0 +1,16 @@ + + + + + Test + + + +

Test

+

$message

+ + + diff --git a/genshi/template/tests/templates/test.txt b/genshi/template/tests/templates/test.txt new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/test.txt @@ -0,0 +1,4 @@ +Test +==== + +$message