Edgewall Software

source: branches/stable/0.5.x/genshi/template/loader.py

Last change on this file was 998, checked in by cmlenz, 15 years ago

Ported [914], [970], and [971] to 0.5.x branch.

  • Property svn:eol-style set to native
File size: 14.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Template loading and caching."""
15
16import os
17try:
18    import threading
19except ImportError:
20    import dummy_threading as threading
21
22from genshi.template.base import TemplateError
23from genshi.util import LRUCache
24
25__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package',
26           'prefixed']
27__docformat__ = 'restructuredtext en'
28
29
30class TemplateNotFound(TemplateError):
31    """Exception raised when a specific template file could not be found."""
32
33    def __init__(self, name, search_path):
34        """Create the exception.
35       
36        :param name: the filename of the template
37        :param search_path: the search path used to lookup the template
38        """
39        TemplateError.__init__(self, 'Template "%s" not found' % name)
40        self.search_path = search_path
41
42
43class TemplateLoader(object):
44    """Responsible for loading templates from files on the specified search
45    path.
46   
47    >>> import tempfile
48    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
49    >>> os.write(fd, '<p>$var</p>')
50    11
51    >>> os.close(fd)
52   
53    The template loader accepts a list of directory paths that are then used
54    when searching for template files, in the given order:
55   
56    >>> loader = TemplateLoader([os.path.dirname(path)])
57   
58    The `load()` method first checks the template cache whether the requested
59    template has already been loaded. If not, it attempts to locate the
60    template file, and returns the corresponding `Template` object:
61   
62    >>> from genshi.template import MarkupTemplate
63    >>> template = loader.load(os.path.basename(path))
64    >>> isinstance(template, MarkupTemplate)
65    True
66   
67    Template instances are cached: requesting a template with the same name
68    results in the same instance being returned:
69   
70    >>> loader.load(os.path.basename(path)) is template
71    True
72   
73    The `auto_reload` option can be used to control whether a template should
74    be automatically reloaded when the file it was loaded from has been
75    changed. Disable this automatic reloading to improve performance.
76   
77    >>> os.remove(path)
78    """
79    def __init__(self, search_path=None, auto_reload=False,
80                 default_encoding=None, max_cache_size=25, default_class=None,
81                 variable_lookup='strict', allow_exec=True, callback=None):
82        """Create the template laoder.
83       
84        :param search_path: a list of absolute path names that should be
85                            searched for template files, or a string containing
86                            a single absolute path; alternatively, any item on
87                            the list may be a ''load function'' that is passed
88                            a filename and returns a file-like object and some
89                            metadata
90        :param auto_reload: whether to check the last modification time of
91                            template files, and reload them if they have changed
92        :param default_encoding: the default encoding to assume when loading
93                                 templates; defaults to UTF-8
94        :param max_cache_size: the maximum number of templates to keep in the
95                               cache
96        :param default_class: the default `Template` subclass to use when
97                              instantiating templates
98        :param variable_lookup: the variable lookup mechanism; either "strict"
99                                (the default), "lenient", or a custom lookup
100                                class
101        :param allow_exec: whether to allow Python code blocks in templates
102        :param callback: (optional) a callback function that is invoked after a
103                         template was initialized by this loader; the function
104                         is passed the template object as only argument. This
105                         callback can be used for example to add any desired
106                         filters to the template
107        :see: `LenientLookup`, `StrictLookup`
108       
109        :note: Changed in 0.5: Added the `allow_exec` argument
110        """
111        from genshi.template.markup import MarkupTemplate
112
113        self.search_path = search_path
114        if self.search_path is None:
115            self.search_path = []
116        elif not isinstance(self.search_path, (list, tuple)):
117            self.search_path = [self.search_path]
118
119        self.auto_reload = auto_reload
120        """Whether templates should be reloaded when the underlying file is
121        changed"""
122
123        self.default_encoding = default_encoding
124        self.default_class = default_class or MarkupTemplate
125        self.variable_lookup = variable_lookup
126        self.allow_exec = allow_exec
127        if callback is not None and not hasattr(callback, '__call__'):
128            raise TypeError('The "callback" parameter needs to be callable')
129        self.callback = callback
130        self._cache = LRUCache(max_cache_size)
131        self._uptodate = {}
132        self._lock = threading.RLock()
133
134    def load(self, filename, relative_to=None, cls=None, encoding=None):
135        """Load the template with the given name.
136       
137        If the `filename` parameter is relative, this method searches the
138        search path trying to locate a template matching the given name. If the
139        file name is an absolute path, the search path is ignored.
140       
141        If the requested template is not found, a `TemplateNotFound` exception
142        is raised. Otherwise, a `Template` object is returned that represents
143        the parsed template.
144       
145        Template instances are cached to avoid having to parse the same
146        template file more than once. Thus, subsequent calls of this method
147        with the same template file name will return the same `Template`
148        object (unless the ``auto_reload`` option is enabled and the file was
149        changed since the last parse.)
150       
151        If the `relative_to` parameter is provided, the `filename` is
152        interpreted as being relative to that path.
153       
154        :param filename: the relative path of the template file to load
155        :param relative_to: the filename of the template from which the new
156                            template is being loaded, or ``None`` if the
157                            template is being loaded directly
158        :param cls: the class of the template object to instantiate
159        :param encoding: the encoding of the template to load; defaults to the
160                         ``default_encoding`` of the loader instance
161        :return: the loaded `Template` instance
162        :raises TemplateNotFound: if a template with the given name could not
163                                  be found
164        """
165        if cls is None:
166            cls = self.default_class
167        search_path = self.search_path
168
169        # Make the filename relative to the template file its being loaded
170        # from, but only if that file is specified as a relative path, or no
171        # search path has been set up
172        if relative_to and (not search_path or not os.path.isabs(relative_to)):
173            filename = os.path.join(os.path.dirname(relative_to), filename)
174
175        filename = os.path.normpath(filename)
176        cachekey = filename
177
178        self._lock.acquire()
179        try:
180            # First check the cache to avoid reparsing the same file
181            try:
182                tmpl = self._cache[cachekey]
183                if not self.auto_reload:
184                    return tmpl
185                uptodate = self._uptodate[cachekey]
186                if uptodate is not None and uptodate():
187                    return tmpl
188            except (KeyError, OSError):
189                pass
190
191            isabs = False
192
193            if os.path.isabs(filename):
194                # Bypass the search path if the requested filename is absolute
195                search_path = [os.path.dirname(filename)]
196                isabs = True
197
198            elif relative_to and os.path.isabs(relative_to):
199                # Make sure that the directory containing the including
200                # template is on the search path
201                dirname = os.path.dirname(relative_to)
202                if dirname not in search_path:
203                    search_path = list(search_path) + [dirname]
204                isabs = True
205
206            elif not search_path:
207                # Uh oh, don't know where to look for the template
208                raise TemplateError('Search path for templates not configured')
209
210            for loadfunc in search_path:
211                if isinstance(loadfunc, basestring):
212                    loadfunc = directory(loadfunc)
213                try:
214                    filepath, filename, fileobj, uptodate = loadfunc(filename)
215                except IOError:
216                    continue
217                else:
218                    try:
219                        if isabs:
220                            # If the filename of either the included or the
221                            # including template is absolute, make sure the
222                            # included template gets an absolute path, too,
223                            # so that nested includes work properly without a
224                            # search path
225                            filename = filepath
226                        tmpl = self._instantiate(cls, fileobj, filepath,
227                                                 filename, encoding=encoding)
228                        if self.callback:
229                            self.callback(tmpl)
230                        self._cache[cachekey] = tmpl
231                        self._uptodate[cachekey] = uptodate
232                    finally:
233                        if hasattr(fileobj, 'close'):
234                            fileobj.close()
235                    return tmpl
236
237            raise TemplateNotFound(filename, search_path)
238
239        finally:
240            self._lock.release()
241
242    def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
243        """Instantiate and return the `Template` object based on the given
244        class and parameters.
245       
246        This function is intended for subclasses to override if they need to
247        implement special template instantiation logic. Code that just uses
248        the `TemplateLoader` should use the `load` method instead.
249       
250        :param cls: the class of the template object to instantiate
251        :param fileobj: a readable file-like object containing the template
252                        source
253        :param filepath: the absolute path to the template file
254        :param filename: the path to the template file relative to the search
255                         path
256        :param encoding: the encoding of the template to load; defaults to the
257                         ``default_encoding`` of the loader instance
258        :return: the loaded `Template` instance
259        :rtype: `Template`
260        """
261        if encoding is None:
262            encoding = self.default_encoding
263        return cls(fileobj, filepath=filepath, filename=filename, loader=self,
264                   encoding=encoding, lookup=self.variable_lookup,
265                   allow_exec=self.allow_exec)
266
267    def directory(path):
268        """Loader factory for loading templates from a local directory.
269       
270        :param path: the path to the local directory containing the templates
271        :return: the loader function to load templates from the given directory
272        :rtype: ``function``
273        """
274        def _load_from_directory(filename):
275            filepath = os.path.join(path, filename)
276            fileobj = open(filepath, 'U')
277            mtime = os.path.getmtime(filepath)
278            def _uptodate():
279                return mtime == os.path.getmtime(filepath)
280            return filepath, filename, fileobj, _uptodate
281        return _load_from_directory
282    directory = staticmethod(directory)
283
284    def package(name, path):
285        """Loader factory for loading templates from egg package data.
286       
287        :param name: the name of the package containing the resources
288        :param path: the path inside the package data
289        :return: the loader function to load templates from the given package
290        :rtype: ``function``
291        """
292        from pkg_resources import resource_stream
293        def _load_from_package(filename):
294            filepath = os.path.join(path, filename)
295            return filepath, filename, resource_stream(name, filepath), None
296        return _load_from_package
297    package = staticmethod(package)
298
299    def prefixed(**delegates):
300        """Factory for a load function that delegates to other loaders
301        depending on the prefix of the requested template path.
302       
303        The prefix is stripped from the filename when passing on the load
304        request to the delegate.
305       
306        >>> load = prefixed(
307        ...     app1 = lambda filename: ('app1', filename, None, None),
308        ...     app2 = lambda filename: ('app2', filename, None, None)
309        ... )
310        >>> print load('app1/foo.html')
311        ('app1', 'app1/foo.html', None, None)
312        >>> print load('app2/bar.html')
313        ('app2', 'app2/bar.html', None, None)
314       
315        :param delegates: mapping of path prefixes to loader functions
316        :return: the loader function
317        :rtype: ``function``
318        """
319        def _dispatch_by_prefix(filename):
320            for prefix, delegate in delegates.items():
321                if filename.startswith(prefix):
322                    if isinstance(delegate, basestring):
323                        delegate = directory(delegate)
324                    filepath, _, fileobj, uptodate = delegate(
325                        filename[len(prefix):].lstrip('/\\')
326                    )
327                    return filepath, filename, fileobj, uptodate
328            raise TemplateNotFound(filename, delegates.keys())
329        return _dispatch_by_prefix
330    prefixed = staticmethod(prefixed)
331
332directory = TemplateLoader.directory
333package = TemplateLoader.package
334prefixed = TemplateLoader.prefixed
Note: See TracBrowser for help on using the repository browser.