# HG changeset patch # User cmlenz # Date 1206571763 0 # Node ID 87b8e23610d498a07470710f046020b538c78228 # Parent 3d11d6b1d3d4ac7c2fc613488533a2421988147a 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. diff --git a/ChangeLog b/ChangeLog --- 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 diff --git a/genshi/template/loader.py b/genshi/template/loader.py --- 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 diff --git a/genshi/template/tests/loader.py b/genshi/template/tests/loader.py --- a/genshi/template/tests/loader.py +++ b/genshi/template/tests/loader.py @@ -104,6 +104,34 @@
Included
""", tmpl.generate().render()) + def test_relative_include_samesubdir(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
Included tmpl1.html
""") + 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("""
Included sub/tmpl1.html
""") + finally: + file2.close() + + file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file3.write(""" + + """) + finally: + file3.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('sub/tmpl2.html') + self.assertEqual(""" +
Included sub/tmpl1.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 @@
Included
""", tmpl2.generate().render()) + def test_relative_absolute_template_preferred(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
Included
""") + 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("""
Included from sub
""") + finally: + file2.close() + + file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file3.write(""" + + """) + finally: + file3.close() + + loader = TemplateLoader() + tmpl = loader.load(os.path.abspath(os.path.join(self.dirname, 'sub', + 'tmpl2.html'))) + self.assertEqual(""" +
Included from sub
+ """, 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 @@

Hello, hello

""", 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("""
Included foo
""") + 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(""" + from sub1 + """) + 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(""" + from sub2 + """) + 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(""" +
Included foo
from sub1 + """, tmpl.generate().render()) + def suite(): suite = unittest.TestSuite()