changeset 363:37e4b4bb0b53 trunk

Parse template includes at parse time to avoid some runtime overhead.
author cmlenz
date Wed, 22 Nov 2006 15:38:38 +0000
parents fe40d34fb71d
children 7dabedbb53fb
files genshi/filters.py genshi/path.py genshi/template/core.py genshi/template/loader.py genshi/template/markup.py genshi/template/tests/markup.py genshi/tests/filters.py
diffstat 7 files changed, 178 insertions(+), 145 deletions(-) [+]
line wrap: on
line diff
--- a/genshi/filters.py
+++ b/genshi/filters.py
@@ -19,10 +19,10 @@
     from sets import ImmutableSet as frozenset
 import re
 
-from genshi.core import Attrs, Namespace, stripentities
-from genshi.core import END, END_NS, START, START_NS, TEXT
+from genshi.core import Attrs, stripentities
+from genshi.core import END, START, TEXT
 
-__all__ = ['HTMLFormFiller', 'HTMLSanitizer', 'IncludeFilter']
+__all__ = ['HTMLFormFiller', 'HTMLSanitizer']
 
 
 class HTMLFormFiller(object):
@@ -284,78 +284,3 @@
             else:
                 if not waiting_for:
                     yield kind, data, pos
-
-
-class IncludeFilter(object):
-    """Template filter providing (very) basic XInclude support
-    (see http://www.w3.org/TR/xinclude/) in templates.
-    """
-
-    NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
-
-    def __init__(self, loader):
-        """Initialize the filter.
-        
-        @param loader: the `TemplateLoader` to use for resolving references to
-            external template files
-        """
-        self.loader = loader
-
-    def __call__(self, stream, ctxt=None):
-        """Filter the stream, processing any XInclude directives it may
-        contain.
-        
-        @param stream: the markup event stream to filter
-        @param ctxt: the template context
-        """
-        from genshi.template import TemplateError, TemplateNotFound
-
-        namespace = self.NAMESPACE
-        ns_prefixes = []
-        in_fallback = False
-        include_href = fallback_stream = None
-
-        for kind, data, pos in stream:
-
-            if kind is START and not in_fallback and data[0] in namespace:
-                tag, attrs = data
-                if tag.localname == 'include':
-                    include_href = attrs.get('href')
-                elif tag.localname == 'fallback':
-                    in_fallback = True
-                    fallback_stream = []
-
-            elif kind is END and data in namespace:
-                if data.localname == 'include':
-                    try:
-                        if not include_href:
-                            raise TemplateError('Include misses required '
-                                                'attribute "href"')
-                        template = self.loader.load(include_href,
-                                                    relative_to=pos[0])
-                        for event in template.generate(ctxt):
-                            yield event
-
-                    except TemplateNotFound:
-                        if fallback_stream is None:
-                            raise
-                        for event in fallback_stream:
-                            yield event
-
-                    include_href = None
-                    fallback_stream = None
-
-                elif data.localname == 'fallback':
-                    in_fallback = False
-
-            elif in_fallback:
-                fallback_stream.append((kind, data, pos))
-
-            elif kind is START_NS and data[1] == namespace:
-                ns_prefixes.append(data[0])
-
-            elif kind is END_NS and data in ns_prefixes:
-                ns_prefixes.pop()
-
-            else:
-                yield kind, data, pos
--- a/genshi/path.py
+++ b/genshi/path.py
@@ -237,9 +237,9 @@
 
                         elif steps[cursor][0] is ATTRIBUTE:
                             # If the axis of the next location step is the
-                            # attribute axis, we need to move on to
-                            # processing that step without waiting for the
-                            # next markup event
+                            # attribute axis, we need to move on to processing
+                            # that step without waiting for the next markup
+                            # event
                             continue
 
                     # We're done with this step if it's the last step or the
--- a/genshi/template/core.py
+++ b/genshi/template/core.py
@@ -180,6 +180,7 @@
             self.filepath = os.path.join(basedir, filename)
         else:
             self.filepath = filename
+        self.loader = loader
 
         self.filters = [self._flatten, self._eval]
         self.stream = list(self._prepare(self._parse(encoding)))
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -20,7 +20,6 @@
     import dummy_threading as threading
 
 from genshi.template.core import TemplateError
-from genshi.template.markup import MarkupTemplate
 from genshi.util import LRUCache
 
 __all__ = ['TemplateLoader', 'TemplateNotFound']
@@ -53,6 +52,7 @@
     template has already been loaded. If not, it attempts to locate the
     template file, and returns the corresponding `Template` object:
     
+    >>> from genshi.template import MarkupTemplate
     >>> template = loader.load(os.path.basename(path))
     >>> isinstance(template, MarkupTemplate)
     True
@@ -66,7 +66,7 @@
     >>> os.remove(path)
     """
     def __init__(self, search_path=None, auto_reload=False,
-                 default_encoding=None, max_cache_size=25):
+                 default_encoding=None, max_cache_size=25, default_class=None):
         """Create the template laoder.
         
         @param search_path: a list of absolute path names that should be
@@ -78,7 +78,11 @@
             templates; defaults to UTF-8
         @param max_cache_size: the maximum number of templates to keep in the
             cache
+        @param default_class: the default `Template` subclass to use when
+            instantiating templates
         """
+        from genshi.template.markup import MarkupTemplate
+
         self.search_path = search_path
         if self.search_path is None:
             self.search_path = []
@@ -86,12 +90,12 @@
             self.search_path = [self.search_path]
         self.auto_reload = auto_reload
         self.default_encoding = default_encoding
+        self.default_class = default_class or MarkupTemplate
         self._cache = LRUCache(max_cache_size)
         self._mtime = {}
         self._lock = threading.Lock()
 
-    def load(self, filename, relative_to=None, cls=MarkupTemplate,
-             encoding=None):
+    def load(self, filename, relative_to=None, cls=None, encoding=None):
         """Load the template with the given name.
         
         If the `filename` parameter is relative, this method searches the search
@@ -119,6 +123,8 @@
         @param encoding: the encoding of the template to load; defaults to the
             `default_encoding` of the loader instance
         """
+        if cls is None:
+            cls = self.default_class
         if encoding is None:
             encoding = self.default_encoding
         if relative_to and not os.path.isabs(relative_to):
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -15,12 +15,12 @@
 
 from itertools import chain
 
-from genshi.core import Attrs, Namespace, Stream
+from genshi.core import Attrs, Namespace, Stream, StreamEventKind
 from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
-from genshi.filters import IncludeFilter
 from genshi.input import XMLParser
-from genshi.template.core import BadDirectiveError, Template, _apply_directives
-from genshi.template.core import SUB
+from genshi.template.core import BadDirectiveError, Template, \
+                                 _apply_directives, SUB
+from genshi.template.loader import TemplateNotFound
 from genshi.template.directives import *
 
 
@@ -35,7 +35,10 @@
       <li>1</li><li>2</li><li>3</li>
     </ul>
     """
-    NAMESPACE = Namespace('http://genshi.edgewall.org/')
+    INCLUDE = StreamEventKind('INCLUDE')
+
+    DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
+    XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
 
     directives = [('def', DefDirective),
                   ('match', MatchDirective),
@@ -58,7 +61,7 @@
 
         self.filters.append(self._match)
         if loader:
-            self.filters.append(IncludeFilter(loader))
+            self.filters.append(self._include)
 
     def _parse(self, encoding):
         """Parse the template from an XML document."""
@@ -66,6 +69,9 @@
         dirmap = {} # temporary mapping of directives to elements
         ns_prefix = {}
         depth = 0
+        in_fallback = False
+        fallback_stream = None
+        include_href = None
 
         for kind, data, pos in XMLParser(self.source, filename=self.filename,
                                          encoding=encoding):
@@ -74,32 +80,34 @@
                 # Strip out the namespace declaration for template directives
                 prefix, uri = data
                 ns_prefix[prefix] = uri
-                if uri != self.NAMESPACE:
+                if uri not in (self.DIRECTIVE_NAMESPACE,
+                               self.XINCLUDE_NAMESPACE):
                     stream.append((kind, data, pos))
 
             elif kind is END_NS:
                 uri = ns_prefix.pop(data, None)
-                if uri and uri != self.NAMESPACE:
+                if uri and uri not in (self.DIRECTIVE_NAMESPACE,
+                                       self.XINCLUDE_NAMESPACE):
                     stream.append((kind, data, pos))
 
             elif kind is START:
                 # Record any directive attributes in start tags
-                tag, attrib = data
+                tag, attrs = data
                 directives = []
                 strip = False
 
-                if tag in self.NAMESPACE:
+                if tag in self.DIRECTIVE_NAMESPACE:
                     cls = self._dir_by_name.get(tag.localname)
                     if cls is None:
                         raise BadDirectiveError(tag.localname, self.filepath,
                                                 pos[1])
-                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
+                    value = attrs.get(getattr(cls, 'ATTRIBUTE', None), '')
                     directives.append((cls, value, ns_prefix.copy(), pos))
                     strip = True
 
-                new_attrib = []
-                for name, value in attrib:
-                    if name in self.NAMESPACE:
+                new_attrs = []
+                for name, value in attrs:
+                    if name in self.DIRECTIVE_NAMESPACE:
                         cls = self._dir_by_name.get(name.localname)
                         if cls is None:
                             raise BadDirectiveError(name.localname,
@@ -113,19 +121,41 @@
                                 value = value[0][1]
                         else:
                             value = [(TEXT, u'', pos)]
-                        new_attrib.append((name, value))
+                        new_attrs.append((name, value))
+                new_attrs = Attrs(new_attrs)
 
                 if directives:
                     index = self._dir_order.index
                     directives.sort(lambda a, b: cmp(index(a[0]), index(b[0])))
                     dirmap[(depth, tag)] = (directives, len(stream), strip)
 
-                stream.append((kind, (tag, Attrs(new_attrib)), pos))
+                if tag in self.XINCLUDE_NAMESPACE:
+                    if tag.localname == 'include':
+                        include_href = new_attrs.get('href')
+                        if not include_href:
+                            raise TemplateSyntaxError('Include misses required '
+                                                      'attribute "href"', *pos)
+                    elif tag.localname == 'fallback':
+                        in_fallback = True
+                        fallback_stream = []
+
+                else:
+                    stream.append((kind, (tag, new_attrs), pos))
+
                 depth += 1
 
             elif kind is END:
                 depth -= 1
-                stream.append((kind, data, pos))
+
+                if in_fallback:
+                    if data == self.XINCLUDE_NAMESPACE['fallback']:
+                        in_fallback = False
+                    else:
+                        fallback_stream.append((kind, data, pos))
+                elif data == self.XINCLUDE_NAMESPACE['include']:
+                    stream.append((INCLUDE, (include_href, fallback_stream), pos))
+                else:
+                    stream.append((kind, data, pos))
 
                 # If there have have directive attributes with the corresponding
                 # start tag, move the events inbetween into a "subprogram"
@@ -151,6 +181,31 @@
 
         return stream
 
+    def _include(self, stream, ctxt):
+        """Internal stream filter that performs inclusion of external
+        template files.
+        """
+        for event in stream:
+            if event[0] is INCLUDE:
+                href, fallback = event[1]
+                if not isinstance(href, basestring):
+                    parts = []
+                    for subkind, subdata, subpos in self._eval(href, ctxt):
+                        if subkind is TEXT:
+                            parts.append(subdata)
+                    href = u''.join([x for x in parts if x is not None])
+                try:
+                    tmpl = self.loader.load(href, relative_to=event[2][0])
+                    for event in tmpl.generate(ctxt):
+                        yield event
+                except TemplateNotFound:
+                    if fallback is None:
+                        raise
+                    for event in fallback:
+                        yield event
+            else:
+                yield event
+
     def _match(self, stream, ctxt, match_templates=None):
         """Internal stream filter that applies any defined match templates
         to the stream.
@@ -196,9 +251,7 @@
                     # corresponding to this start event is encountered
                     content = chain([event], self._match(_strip(stream), ctxt),
                                     tail)
-                    for filter_ in self.filters[3:]:
-                        content = filter_(content, ctxt)
-                    content = list(content)
+                    content = list(self._include(content, ctxt))
 
                     for test in [mt[0] for mt in match_templates]:
                         test(tail[0], namespaces, ctxt, updateonly=True)
@@ -223,3 +276,6 @@
 
             else: # no matches
                 yield event
+
+
+INCLUDE = MarkupTemplate.INCLUDE
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -12,11 +12,15 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
+import os
+import shutil
 import sys
+import tempfile
 import unittest
 
 from genshi.core import Markup
 from genshi.template.core import BadDirectiveError, TemplateSyntaxError
+from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 
 
@@ -179,6 +183,87 @@
           \xf6
         </div>""", unicode(tmpl.generate()))
 
+    def test_include_in_loop(self):
+        dirname = tempfile.mkdtemp(suffix='genshi_test')
+        try:
+            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
+            try:
+                file1.write("""<div>Included $idx</div>""")
+            finally:
+                file1.close()
+
+            file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w')
+            try:
+                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                     xmlns:py="http://genshi.edgewall.org/">
+                  <xi:include href="${name}.html" py:for="idx in range(3)" />
+                </html>""")
+            finally:
+                file2.close()
+
+            loader = TemplateLoader([dirname])
+            tmpl = loader.load('tmpl2.html')
+            self.assertEqual("""<html>
+                  <div>Included 0</div><div>Included 1</div><div>Included 2</div>
+                </html>""", tmpl.generate(name='tmpl1').render())
+        finally:
+            shutil.rmtree(dirname)
+
+    def test_dynamic_inlude_href(self):
+        dirname = tempfile.mkdtemp(suffix='genshi_test')
+        try:
+            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
+            try:
+                file1.write("""<div>Included</div>""")
+            finally:
+                file1.close()
+
+            file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w')
+            try:
+                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                     xmlns:py="http://genshi.edgewall.org/">
+                  <xi:include href="${name}.html" />
+                </html>""")
+            finally:
+                file2.close()
+
+            loader = TemplateLoader([dirname])
+            tmpl = loader.load('tmpl2.html')
+            self.assertEqual("""<html>
+                  <div>Included</div>
+                </html>""", tmpl.generate(name='tmpl1').render())
+        finally:
+            shutil.rmtree(dirname)
+
+    def test_select_inluded_elements(self):
+        dirname = tempfile.mkdtemp(suffix='genshi_test')
+        try:
+            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
+            try:
+                file1.write("""<li>$item</li>""")
+            finally:
+                file1.close()
+
+            file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w')
+            try:
+                file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
+                                     xmlns:py="http://genshi.edgewall.org/">
+                  <ul py:match="ul">${select('li')}</ul>
+                  <ul py:with="items=(1, 2, 3)">
+                    <xi:include href="tmpl1.html" py:for="item in items" />
+                  </ul>
+                </html>""")
+            finally:
+                file2.close()
+
+            loader = TemplateLoader([dirname])
+            tmpl = loader.load('tmpl2.html')
+            self.assertEqual("""<html>
+                  <ul><li>1</li><li>2</li><li>3</li></ul>
+                </html>""", tmpl.generate().render())
+        finally:
+            shutil.rmtree(dirname)
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/tests/filters.py
+++ b/genshi/tests/filters.py
@@ -12,16 +12,11 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
-import os
-import shutil
-import tempfile
 import unittest
 
 from genshi import filters
-from genshi.core import Stream
 from genshi.input import HTML, ParseError
 from genshi.filters import HTMLFormFiller, HTMLSanitizer
-from genshi.template import TemplateLoader
 
 
 class HTMLFormFillerTestCase(unittest.TestCase):
@@ -374,46 +369,11 @@
         self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
 
 
-class IncludeFilterTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.dirname = tempfile.mkdtemp(suffix='markup_test')
-
-    def tearDown(self):
-        shutil.rmtree(self.dirname)
-
-    def test_select_inluded_elements(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<li>$item</li>""")
-        finally:
-            file1.close()
-
-        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
-                                 xmlns:py="http://genshi.edgewall.org/">
-              <ul py:match="ul">${select('li')}</ul>
-              <ul py:with="items=(1, 2, 3)">
-                <xi:include href="tmpl1.html" py:for="item in items" />
-              </ul>
-            </html>""")
-        finally:
-            file2.close()
-
-        loader = TemplateLoader([self.dirname])
-        tmpl = loader.load('tmpl2.html')
-        self.assertEqual("""<html>
-              <ul><li>1</li><li>2</li><li>3</li></ul>
-            </html>""", tmpl.generate().render())
-
-
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(filters))
     suite.addTest(unittest.makeSuite(HTMLFormFillerTestCase, 'test'))
     suite.addTest(unittest.makeSuite(HTMLSanitizerTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(IncludeFilterTestCase, 'test'))
     return suite
 
 if __name__ == '__main__':
Copyright (C) 2012-2017 Edgewall Software