changeset 696:66eead58c120 trunk

More flexible template loader allowing for loading from package data and dispatching to different template directories based on path prefix. Can be easily extended for using custom template loading. Closes #182.
author cmlenz
date Wed, 26 Mar 2008 22:49:23 +0000
parents ed5044d318ed
children 3d3c322ca978
files ChangeLog genshi/template/loader.py genshi/template/tests/loader.py
diffstat 3 files changed, 187 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -59,6 +59,12 @@
  * The `TemplateLoader` class now provides an `instantiate()` method that can
    be overridden by subclasses to implement advanced template instantiation
    logic (ticket #204).
+ * The search path of the `TemplateLoader` class can now contain ''load
+   functions'' in addition to path strings. A load function is passed the
+   name of the requested template file, and should return a file-like object
+   and some metadata. New load functions are supplied for loading from egg
+   package data, and loading from different loaders depending on the path
+   prefix of the requested filename (ticket #182).
 
 
 Version 0.4.4
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -82,7 +82,10 @@
         
         :param search_path: a list of absolute path names that should be
                             searched for template files, or a string containing
-                            a single absolute path
+                            a single absolute path; alternatively, any item on
+                            the list may be a ''load function'' that is passed
+                            a filename and returns a file-like object and some
+                            metadata
         :param auto_reload: whether to check the last modification time of
                             template files, and reload them if they have changed
         :param default_encoding: the default encoding to assume when loading
@@ -109,7 +112,7 @@
         self.search_path = search_path
         if self.search_path is None:
             self.search_path = []
-        elif isinstance(self.search_path, basestring):
+        elif not isinstance(self.search_path, (list, tuple)):
             self.search_path = [self.search_path]
 
         self.auto_reload = auto_reload
@@ -130,9 +133,9 @@
     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
-        path trying to locate a template matching the given name. If the file
-        name is an absolute path, the search path is ignored.
+        If the `filename` parameter is relative, this method searches the
+        search path trying to locate a template matching the given name. If the
+        file name is an absolute path, the search path is ignored.
         
         If the requested template is not found, a `TemplateNotFound` exception
         is raised. Otherwise, a `Template` object is returned that represents
@@ -169,8 +172,10 @@
             # First check the cache to avoid reparsing the same file
             try:
                 tmpl = self._cache[filename]
-                if not self.auto_reload or \
-                        os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
+                if not self.auto_reload:
+                    return tmpl
+                mtime = self._mtime[filename]
+                if mtime and mtime == os.path.getmtime(tmpl.filepath):
                     return tmpl
             except KeyError, OSError:
                 pass
@@ -195,16 +200,20 @@
                 # Uh oh, don't know where to look for the template
                 raise TemplateError('Search path for templates not configured')
 
-            for dirname in search_path:
-                filepath = os.path.join(dirname, filename)
+            for loadfunc in search_path:
+                if isinstance(loadfunc, basestring):
+                    loadfunc = TemplateLoader.directory(loadfunc)
                 try:
-                    fileobj = open(filepath, 'U')
+                    dirname, filename, fileobj, mtime = loadfunc(filename)
+                except IOError:
+                    continue
+                else:
                     try:
                         if isabs:
                             # If the filename of either the included or the 
                             # including template is absolute, make sure the
                             # included template gets an absolute path, too,
-                            # so that nested include work properly without a
+                            # so that nested includes work properly without a
                             # search path
                             filename = os.path.join(dirname, filename)
                             dirname = ''
@@ -213,12 +222,11 @@
                         if self.callback:
                             self.callback(tmpl)
                         self._cache[filename] = tmpl
-                        self._mtime[filename] = os.path.getmtime(filepath)
+                        self._mtime[filename] = mtime
                     finally:
-                        fileobj.close()
+                        if hasattr(fileobj, 'close'):
+                            fileobj.close()
                     return tmpl
-                except IOError:
-                    continue
 
             raise TemplateNotFound(filename, search_path)
 
@@ -250,3 +258,66 @@
         return cls(fileobj, basedir=dirname, filename=filename, loader=self,
                    encoding=encoding, lookup=self.variable_lookup,
                    allow_exec=self.allow_exec)
+
+    def directory(path):
+        """Loader factory for loading templates from a local directory.
+        
+        :param path: the path to the local directory containing the templates
+        :return: the loader function to load templates from the given directory
+        :rtype: ``function``
+        """
+        def _load_from_directory(filename):
+            filepath = os.path.join(path, filename)
+            fileobj = open(filepath, 'U')
+            return path, filename, fileobj, os.path.getmtime(filepath)
+        return _load_from_directory
+    directory = staticmethod(directory)
+
+    def package(name, path):
+        """Loader factory for loading templates from egg package data.
+        
+        :param name: the name of the package containing the resources
+        :param path: the path inside the package data
+        :return: the loader function to load templates from the given package
+        :rtype: ``function``
+        """
+        from pkg_resources import resource_stream
+        def _load_from_package(filename):
+            filepath = os.path.join(path, filename)
+            return path, filename, resource_stream(name, filepath), None
+        return _load_from_package
+    package = staticmethod(package)
+
+    def prefixed(**delegates):
+        """Factory for a load function that delegates to other loaders
+        depending on the prefix of the requested template path.
+        
+        The prefix is stripped from the filename when passing on the load
+        request to the delegate.
+        
+        >>> load = prefixed(
+        ...     app1 = lambda filename: ('app1', filename),
+        ...     app2 = lambda filename: ('app2', filename)
+        ... )
+        >>> print load('app1/foo.html')
+        ('app1', 'foo.html')
+        >>> print load('app2/bar.html')
+        ('app2', 'bar.html')
+
+        :param delegates: mapping of path prefixes to loader functions
+        :return: the loader function
+        :rtype: ``function``
+        """
+        def _dispatch_by_prefix(filename):
+            for prefix, delegate in delegates.items():
+                if filename.startswith(prefix):
+                    if isinstance(delegate, basestring):
+                        delegate = TemplateLoader.directory(delegate)
+                    return delegate(filename[len(prefix):].lstrip('/\\'))
+            raise TemplateNotFound(filename, delegates.keys())
+        return _dispatch_by_prefix
+    prefixed = staticmethod(prefixed)
+
+directory = TemplateLoader.directory
+package = TemplateLoader.package
+prefixed = TemplateLoader.prefixed
--- a/genshi/template/tests/loader.py
+++ b/genshi/template/tests/loader.py
@@ -104,6 +104,34 @@
               <div>Included</div>
             </html>""", tmpl.generate().render())
 
+    def test_relative_include_samesubdir(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included tmpl1.html</div>""")
+        finally:
+            file1.close()
+
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<div>Included sub/tmpl1.html</div>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('sub/tmpl2.html')
+        self.assertEqual("""<html>
+              <div>Included sub/tmpl1.html</div>
+            </html>""", tmpl.generate().render())
+
     def test_relative_include_without_search_path(self):
         file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
         try:
@@ -172,6 +200,35 @@
           <div>Included</div>
         </html>""", tmpl2.generate().render())
 
+    def test_relative_absolute_template_preferred(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<div>Included from sub</div>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader()
+        tmpl = loader.load(os.path.abspath(os.path.join(self.dirname, 'sub',
+                                                        'tmpl2.html')))
+        self.assertEqual("""<html>
+              <div>Included from sub</div>
+            </html>""", tmpl.generate().render())
+
     def test_load_with_default_encoding(self):
         f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
         try:
@@ -219,6 +276,44 @@
               <p>Hello, hello</p>
             </html>""", tmpl.generate().render())
 
+    def test_prefix_delegation_to_directories(self):
+        dir1 = os.path.join(self.dirname, 'templates')
+        os.mkdir(dir1)
+        file1 = open(os.path.join(dir1, 'foo.html'), 'w')
+        try:
+            file1.write("""<div>Included foo</div>""")
+        finally:
+            file1.close()
+
+        dir2 = os.path.join(self.dirname, 'sub1', 'templates')
+        os.makedirs(dir2)
+        file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="foo.html" /> from sub1
+            </html>""")
+        finally:
+            file2.close()
+
+        dir3 = os.path.join(self.dirname, 'sub2', 'templates')
+        os.makedirs(dir3)
+        file3 = open(os.path.join(dir3, 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="foo.html" /> from sub2
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader([dir1, TemplateLoader.prefixed(
+            sub1 = os.path.join(dir2),
+            sub2 = os.path.join(dir3)
+        )])
+        tmpl = loader.load('sub1/tmpl1.html')
+        self.assertEqual("""<html>
+              <div>Included foo</div> from sub1
+            </html>""", tmpl.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
Copyright (C) 2012-2017 Edgewall Software