Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/websdk/genshi/template/loader.py
blob: 0e7cda7445cd8055de84328152b9007d400dcb43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# -*- coding: utf-8 -*-
#
# Copyright (C) 2006-2010 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://genshi.edgewall.org/wiki/License.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://genshi.edgewall.org/log/.

"""Template loading and caching."""

import os
try:
    import threading
except ImportError:
    import dummy_threading as threading

from genshi.template.base import TemplateError
from genshi.util import LRUCache

__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package',
           'prefixed']
__docformat__ = 'restructuredtext en'


class TemplateNotFound(TemplateError):
    """Exception raised when a specific template file could not be found."""

    def __init__(self, name, search_path):
        """Create the exception.
        
        :param name: the filename of the template
        :param search_path: the search path used to lookup the template
        """
        TemplateError.__init__(self, 'Template "%s" not found' % name)
        self.search_path = search_path


class TemplateLoader(object):
    """Responsible for loading templates from files on the specified search
    path.
    
    >>> import tempfile
    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
    >>> os.write(fd, '<p>$var</p>')
    11
    >>> os.close(fd)
    
    The template loader accepts a list of directory paths that are then used
    when searching for template files, in the given order:
    
    >>> loader = TemplateLoader([os.path.dirname(path)])
    
    The `load()` method first checks the template cache whether the requested
    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
    
    Template instances are cached: requesting a template with the same name
    results in the same instance being returned:
    
    >>> loader.load(os.path.basename(path)) is template
    True
    
    The `auto_reload` option can be used to control whether a template should
    be automatically reloaded when the file it was loaded from has been
    changed. Disable this automatic reloading to improve performance.
    
    >>> os.remove(path)
    """
    def __init__(self, search_path=None, auto_reload=False,
                 default_encoding=None, max_cache_size=25, default_class=None,
                 variable_lookup='strict', allow_exec=True, callback=None):
        """Create the template laoder.
        
        :param search_path: a list of absolute path names that should be
                            searched for template files, or a string containing
                            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
                                 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
        :param variable_lookup: the variable lookup mechanism; either "strict"
                                (the default), "lenient", or a custom lookup
                                class
        :param allow_exec: whether to allow Python code blocks in templates
        :param callback: (optional) a callback function that is invoked after a
                         template was initialized by this loader; the function
                         is passed the template object as only argument. This
                         callback can be used for example to add any desired
                         filters to the template
        :see: `LenientLookup`, `StrictLookup`
        
        :note: Changed in 0.5: Added the `allow_exec` argument
        """
        from genshi.template.markup import MarkupTemplate

        self.search_path = search_path
        if self.search_path is None:
            self.search_path = []
        elif not isinstance(self.search_path, (list, tuple)):
            self.search_path = [self.search_path]

        self.auto_reload = auto_reload
        """Whether templates should be reloaded when the underlying file is
        changed"""

        self.default_encoding = default_encoding
        self.default_class = default_class or MarkupTemplate
        self.variable_lookup = variable_lookup
        self.allow_exec = allow_exec
        if callback is not None and not hasattr(callback, '__call__'):
            raise TypeError('The "callback" parameter needs to be callable')
        self.callback = callback
        self._cache = LRUCache(max_cache_size)
        self._uptodate = {}
        self._lock = threading.RLock()

    def __getstate__(self):
        state = self.__dict__.copy()
        state['_lock'] = None
        return state

    def __setstate__(self, state):
        self.__dict__ = state
        self._lock = threading.RLock()

    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 requested template is not found, a `TemplateNotFound` exception
        is raised. Otherwise, a `Template` object is returned that represents
        the parsed template.
        
        Template instances are cached to avoid having to parse the same
        template file more than once. Thus, subsequent calls of this method
        with the same template file name will return the same `Template`
        object (unless the ``auto_reload`` option is enabled and the file was
        changed since the last parse.)
        
        If the `relative_to` parameter is provided, the `filename` is
        interpreted as being relative to that path.
        
        :param filename: the relative path of the template file to load
        :param relative_to: the filename of the template from which the new
                            template is being loaded, or ``None`` if the
                            template is being loaded directly
        :param cls: the class of the template object to instantiate
        :param encoding: the encoding of the template to load; defaults to the
                         ``default_encoding`` of the loader instance
        :return: the loaded `Template` instance
        :raises TemplateNotFound: if a template with the given name could not
                                  be found
        """
        if cls is None:
            cls = self.default_class
        search_path = self.search_path

        # Make the filename relative to the template file its being loaded
        # from, but only if that file is specified as a relative path, or no
        # search path has been set up
        if relative_to and (not search_path or not os.path.isabs(relative_to)):
            filename = os.path.join(os.path.dirname(relative_to), filename)

        filename = os.path.normpath(filename)
        cachekey = filename

        self._lock.acquire()
        try:
            # First check the cache to avoid reparsing the same file
            try:
                tmpl = self._cache[cachekey]
                if not self.auto_reload:
                    return tmpl
                uptodate = self._uptodate[cachekey]
                if uptodate is not None and uptodate():
                    return tmpl
            except (KeyError, OSError):
                pass

            isabs = False

            if os.path.isabs(filename):
                # Bypass the search path if the requested filename is absolute
                search_path = [os.path.dirname(filename)]
                isabs = True

            elif relative_to and os.path.isabs(relative_to):
                # Make sure that the directory containing the including
                # template is on the search path
                dirname = os.path.dirname(relative_to)
                if dirname not in search_path:
                    search_path = list(search_path) + [dirname]
                isabs = True

            elif not search_path:
                # Uh oh, don't know where to look for the template
                raise TemplateError('Search path for templates not configured')

            for loadfunc in search_path:
                if isinstance(loadfunc, basestring):
                    loadfunc = directory(loadfunc)
                try:
                    filepath, filename, fileobj, uptodate = 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 includes work properly without a
                            # search path
                            filename = filepath
                        tmpl = self._instantiate(cls, fileobj, filepath,
                                                 filename, encoding=encoding)
                        if self.callback:
                            self.callback(tmpl)
                        self._cache[cachekey] = tmpl
                        self._uptodate[cachekey] = uptodate
                    finally:
                        if hasattr(fileobj, 'close'):
                            fileobj.close()
                    return tmpl

            raise TemplateNotFound(filename, search_path)

        finally:
            self._lock.release()

    def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
        """Instantiate and return the `Template` object based on the given
        class and parameters.
        
        This function is intended for subclasses to override if they need to
        implement special template instantiation logic. Code that just uses
        the `TemplateLoader` should use the `load` method instead.
        
        :param cls: the class of the template object to instantiate
        :param fileobj: a readable file-like object containing the template
                        source
        :param filepath: the absolute path to the template file
        :param filename: the path to the template file relative to the search
                         path
        :param encoding: the encoding of the template to load; defaults to the
                         ``default_encoding`` of the loader instance
        :return: the loaded `Template` instance
        :rtype: `Template`
        """
        if encoding is None:
            encoding = self.default_encoding
        return cls(fileobj, filepath=filepath, filename=filename, loader=self,
                   encoding=encoding, lookup=self.variable_lookup,
                   allow_exec=self.allow_exec)

    @staticmethod
    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')
            mtime = os.path.getmtime(filepath)
            def _uptodate():
                return mtime == os.path.getmtime(filepath)
            return filepath, filename, fileobj, _uptodate
        return _load_from_directory

    @staticmethod
    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 filepath, filename, resource_stream(name, filepath), None
        return _load_from_package

    @staticmethod
    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, None, None),
        ...     app2 = lambda filename: ('app2', filename, None, None)
        ... )
        >>> print(load('app1/foo.html'))
        ('app1', 'app1/foo.html', None, None)
        >>> print(load('app2/bar.html'))
        ('app2', 'app2/bar.html', None, None)
        
        :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 = directory(delegate)
                    filepath, _, fileobj, uptodate = delegate(
                        filename[len(prefix):].lstrip('/\\')
                    )
                    return filepath, filename, fileobj, uptodate
            raise TemplateNotFound(filename, list(delegates.keys()))
        return _dispatch_by_prefix


directory = TemplateLoader.directory
package = TemplateLoader.package
prefixed = TemplateLoader.prefixed