Edgewall Software

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

Last change on this file was 705, checked in by cmlenz, 16 years ago

Ported [704] to 0.4.x branch.

  • Property svn:eol-style set to native
File size: 9.1 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 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']
26__docformat__ = 'restructuredtext en'
27
28
29class TemplateNotFound(TemplateError):
30    """Exception raised when a specific template file could not be found."""
31
32    def __init__(self, name, search_path):
33        """Create the exception.
34       
35        :param name: the filename of the template
36        :param search_path: the search path used to lookup the template
37        """
38        TemplateError.__init__(self, 'Template "%s" not found' % name)
39        self.search_path = search_path
40
41
42class TemplateLoader(object):
43    """Responsible for loading templates from files on the specified search
44    path.
45   
46    >>> import tempfile
47    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
48    >>> os.write(fd, '<p>$var</p>')
49    11
50    >>> os.close(fd)
51   
52    The template loader accepts a list of directory paths that are then used
53    when searching for template files, in the given order:
54   
55    >>> loader = TemplateLoader([os.path.dirname(path)])
56   
57    The `load()` method first checks the template cache whether the requested
58    template has already been loaded. If not, it attempts to locate the
59    template file, and returns the corresponding `Template` object:
60   
61    >>> from genshi.template import MarkupTemplate
62    >>> template = loader.load(os.path.basename(path))
63    >>> isinstance(template, MarkupTemplate)
64    True
65   
66    Template instances are cached: requesting a template with the same name
67    results in the same instance being returned:
68   
69    >>> loader.load(os.path.basename(path)) is template
70    True
71   
72    >>> os.remove(path)
73    """
74    def __init__(self, search_path=None, auto_reload=False,
75                 default_encoding=None, max_cache_size=25, default_class=None,
76                 variable_lookup='lenient', callback=None):
77        """Create the template laoder.
78       
79        :param search_path: a list of absolute path names that should be
80                            searched for template files, or a string containing
81                            a single absolute path
82        :param auto_reload: whether to check the last modification time of
83                            template files, and reload them if they have changed
84        :param default_encoding: the default encoding to assume when loading
85                                 templates; defaults to UTF-8
86        :param max_cache_size: the maximum number of templates to keep in the
87                               cache
88        :param default_class: the default `Template` subclass to use when
89                              instantiating templates
90        :param variable_lookup: the variable lookup mechanism; either "lenient"
91                                (the default), "strict", or a custom lookup
92                                class
93        :param callback: (optional) a callback function that is invoked after a
94                         template was initialized by this loader; the function
95                         is passed the template object as only argument. This
96                         callback can be used for example to add any desired
97                         filters to the template
98        :see: `LenientLookup`, `StrictLookup`
99        """
100        from genshi.template.markup import MarkupTemplate
101
102        self.search_path = search_path
103        if self.search_path is None:
104            self.search_path = []
105        elif isinstance(self.search_path, basestring):
106            self.search_path = [self.search_path]
107        self.auto_reload = auto_reload
108        self.default_encoding = default_encoding
109        self.default_class = default_class or MarkupTemplate
110        self.variable_lookup = variable_lookup
111        if callback is not None and not callable(callback):
112            raise TypeError('The "callback" parameter needs to be callable')
113        self.callback = callback
114        self._cache = LRUCache(max_cache_size)
115        self._mtime = {}
116        self._lock = threading.Lock()
117
118    def load(self, filename, relative_to=None, cls=None, encoding=None):
119        """Load the template with the given name.
120       
121        If the `filename` parameter is relative, this method searches the search
122        path trying to locate a template matching the given name. If the file
123        name is an absolute path, the search path is ignored.
124       
125        If the requested template is not found, a `TemplateNotFound` exception
126        is raised. Otherwise, a `Template` object is returned that represents
127        the parsed template.
128       
129        Template instances are cached to avoid having to parse the same
130        template file more than once. Thus, subsequent calls of this method
131        with the same template file name will return the same `Template`
132        object (unless the ``auto_reload`` option is enabled and the file was
133        changed since the last parse.)
134       
135        If the `relative_to` parameter is provided, the `filename` is
136        interpreted as being relative to that path.
137       
138        :param filename: the relative path of the template file to load
139        :param relative_to: the filename of the template from which the new
140                            template is being loaded, or ``None`` if the
141                            template is being loaded directly
142        :param cls: the class of the template object to instantiate
143        :param encoding: the encoding of the template to load; defaults to the
144                         ``default_encoding`` of the loader instance
145        :return: the loaded `Template` instance
146        :raises TemplateNotFound: if a template with the given name could not be
147                                  found
148        """
149        if cls is None:
150            cls = self.default_class
151        if encoding is None:
152            encoding = self.default_encoding
153        if relative_to and not os.path.isabs(relative_to):
154            filename = os.path.join(os.path.dirname(relative_to), filename)
155        filename = os.path.normpath(filename)
156
157        self._lock.acquire()
158        try:
159            # First check the cache to avoid reparsing the same file
160            try:
161                tmpl = self._cache[filename]
162                if not self.auto_reload or \
163                        os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
164                    return tmpl
165            except KeyError, OSError:
166                pass
167
168            search_path = self.search_path
169            isabs = False
170
171            if os.path.isabs(filename):
172                # Bypass the search path if the requested filename is absolute
173                search_path = [os.path.dirname(filename)]
174                isabs = True
175
176            elif relative_to and os.path.isabs(relative_to):
177                # Make sure that the directory containing the including
178                # template is on the search path
179                dirname = os.path.dirname(relative_to)
180                if dirname not in search_path:
181                    search_path = search_path + [dirname]
182                isabs = True
183
184            elif not search_path:
185                # Uh oh, don't know where to look for the template
186                raise TemplateError('Search path for templates not configured')
187
188            for dirname in search_path:
189                filepath = os.path.join(dirname, filename)
190                try:
191                    fileobj = open(filepath, 'U')
192                    try:
193                        if isabs:
194                            # If the filename of either the included or the
195                            # including template is absolute, make sure the
196                            # included template gets an absolute path, too,
197                            # so that nested include work properly without a
198                            # search path
199                            filename = os.path.join(dirname, filename)
200                            dirname = ''
201                        tmpl = cls(fileobj, basedir=dirname, filename=filename,
202                                   loader=self, lookup=self.variable_lookup,
203                                   encoding=encoding)
204                        if self.callback:
205                            self.callback(tmpl)
206                        self._cache[filename] = tmpl
207                        self._mtime[filename] = os.path.getmtime(filepath)
208                    finally:
209                        fileobj.close()
210                    return tmpl
211                except IOError:
212                    continue
213
214            raise TemplateNotFound(filename, search_path)
215
216        finally:
217            self._lock.release()
Note: See TracBrowser for help on using the repository browser.