Edgewall Software

source: trunk/genshi/template/loader.py

Last change on this file was 1190, checked in by hodgestar, 11 years ago

Templates should be loaded as bytes -- the encoding is passed around with the source explicitly. Fixes bugs in test suite in Python 3.3.

  • Property svn:eol-style set to native
File size: 14.4 KB
RevLine 
[414]1# -*- coding: utf-8 -*-
2#
[1120]3# Copyright (C) 2006-2010 Edgewall Software
[414]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
[492]22from genshi.template.base import TemplateError
[414]23from genshi.util import LRUCache
24
[899]25__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package',
26           'prefixed']
[517]27__docformat__ = 'restructuredtext en'
[414]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):
[527]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        """
[414]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')
[1160]49    >>> os.write(fd, u'<p>$var</p>'.encode('utf-8'))
[414]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   
[443]62    >>> from genshi.template import MarkupTemplate
[414]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   
[657]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   
[414]77    >>> os.remove(path)
78    """
79    def __init__(self, search_path=None, auto_reload=False,
[531]80                 default_encoding=None, max_cache_size=25, default_class=None,
[722]81                 variable_lookup='strict', allow_exec=True, callback=None):
[414]82        """Create the template laoder.
83       
[517]84        :param search_path: a list of absolute path names that should be
85                            searched for template files, or a string containing
[812]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
[517]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
[722]98        :param variable_lookup: the variable lookup mechanism; either "strict"
99                                (the default), "lenient", or a custom lookup
[534]100                                class
[654]101        :param allow_exec: whether to allow Python code blocks in templates
[531]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
[534]107        :see: `LenientLookup`, `StrictLookup`
[654]108       
109        :note: Changed in 0.5: Added the `allow_exec` argument
[414]110        """
[443]111        from genshi.template.markup import MarkupTemplate
112
[414]113        self.search_path = search_path
114        if self.search_path is None:
115            self.search_path = []
[812]116        elif not isinstance(self.search_path, (list, tuple)):
[414]117            self.search_path = [self.search_path]
[657]118
[414]119        self.auto_reload = auto_reload
[657]120        """Whether templates should be reloaded when the underlying file is
121        changed"""
122
[414]123        self.default_encoding = default_encoding
[443]124        self.default_class = default_class or MarkupTemplate
[534]125        self.variable_lookup = variable_lookup
[654]126        self.allow_exec = allow_exec
[971]127        if callback is not None and not hasattr(callback, '__call__'):
[531]128            raise TypeError('The "callback" parameter needs to be callable')
129        self.callback = callback
[414]130        self._cache = LRUCache(max_cache_size)
[830]131        self._uptodate = {}
[657]132        self._lock = threading.RLock()
[414]133
[1099]134    def __getstate__(self):
135        state = self.__dict__.copy()
136        state['_lock'] = None
137        return state
138
139    def __setstate__(self, state):
140        self.__dict__ = state
141        self._lock = threading.RLock()
142
[443]143    def load(self, filename, relative_to=None, cls=None, encoding=None):
[414]144        """Load the template with the given name.
145       
[812]146        If the `filename` parameter is relative, this method searches the
147        search path trying to locate a template matching the given name. If the
148        file name is an absolute path, the search path is ignored.
[414]149       
[527]150        If the requested template is not found, a `TemplateNotFound` exception
151        is raised. Otherwise, a `Template` object is returned that represents
152        the parsed template.
[414]153       
154        Template instances are cached to avoid having to parse the same
155        template file more than once. Thus, subsequent calls of this method
156        with the same template file name will return the same `Template`
[517]157        object (unless the ``auto_reload`` option is enabled and the file was
[414]158        changed since the last parse.)
159       
160        If the `relative_to` parameter is provided, the `filename` is
161        interpreted as being relative to that path.
162       
[517]163        :param filename: the relative path of the template file to load
164        :param relative_to: the filename of the template from which the new
[527]165                            template is being loaded, or ``None`` if the
166                            template is being loaded directly
[517]167        :param cls: the class of the template object to instantiate
168        :param encoding: the encoding of the template to load; defaults to the
169                         ``default_encoding`` of the loader instance
[527]170        :return: the loaded `Template` instance
[811]171        :raises TemplateNotFound: if a template with the given name could not
172                                  be found
[414]173        """
[443]174        if cls is None:
175            cls = self.default_class
[895]176        search_path = self.search_path
177
178        # Make the filename relative to the template file its being loaded
179        # from, but only if that file is specified as a relative path, or no
180        # search path has been set up
181        if relative_to and (not search_path or not os.path.isabs(relative_to)):
[414]182            filename = os.path.join(os.path.dirname(relative_to), filename)
[895]183
[414]184        filename = os.path.normpath(filename)
[815]185        cachekey = filename
[414]186
187        self._lock.acquire()
188        try:
189            # First check the cache to avoid reparsing the same file
190            try:
[815]191                tmpl = self._cache[cachekey]
[812]192                if not self.auto_reload:
[414]193                    return tmpl
[830]194                uptodate = self._uptodate[cachekey]
195                if uptodate is not None and uptodate():
[812]196                    return tmpl
[845]197            except (KeyError, OSError):
[414]198                pass
199
200            isabs = False
201
202            if os.path.isabs(filename):
203                # Bypass the search path if the requested filename is absolute
204                search_path = [os.path.dirname(filename)]
205                isabs = True
206
207            elif relative_to and os.path.isabs(relative_to):
208                # Make sure that the directory containing the including
209                # template is on the search path
210                dirname = os.path.dirname(relative_to)
211                if dirname not in search_path:
[813]212                    search_path = list(search_path) + [dirname]
[414]213                isabs = True
214
215            elif not search_path:
216                # Uh oh, don't know where to look for the template
217                raise TemplateError('Search path for templates not configured')
218
[812]219            for loadfunc in search_path:
220                if isinstance(loadfunc, basestring):
[814]221                    loadfunc = directory(loadfunc)
[414]222                try:
[830]223                    filepath, filename, fileobj, uptodate = loadfunc(filename)
[812]224                except IOError:
225                    continue
226                else:
[414]227                    try:
228                        if isabs:
229                            # If the filename of either the included or the
230                            # including template is absolute, make sure the
231                            # included template gets an absolute path, too,
[812]232                            # so that nested includes work properly without a
[414]233                            # search path
[830]234                            filename = filepath
235                        tmpl = self._instantiate(cls, fileobj, filepath,
236                                                 filename, encoding=encoding)
[531]237                        if self.callback:
238                            self.callback(tmpl)
[815]239                        self._cache[cachekey] = tmpl
[830]240                        self._uptodate[cachekey] = uptodate
[414]241                    finally:
[812]242                        if hasattr(fileobj, 'close'):
243                            fileobj.close()
[414]244                    return tmpl
245
246            raise TemplateNotFound(filename, search_path)
247
248        finally:
249            self._lock.release()
[811]250
[830]251    def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
[811]252        """Instantiate and return the `Template` object based on the given
253        class and parameters.
254       
255        This function is intended for subclasses to override if they need to
256        implement special template instantiation logic. Code that just uses
257        the `TemplateLoader` should use the `load` method instead.
258       
259        :param cls: the class of the template object to instantiate
260        :param fileobj: a readable file-like object containing the template
261                        source
[830]262        :param filepath: the absolute path to the template file
263        :param filename: the path to the template file relative to the search
264                         path
[811]265        :param encoding: the encoding of the template to load; defaults to the
266                         ``default_encoding`` of the loader instance
267        :return: the loaded `Template` instance
268        :rtype: `Template`
269        """
270        if encoding is None:
271            encoding = self.default_encoding
[830]272        return cls(fileobj, filepath=filepath, filename=filename, loader=self,
[811]273                   encoding=encoding, lookup=self.variable_lookup,
274                   allow_exec=self.allow_exec)
[812]275
[1031]276    @staticmethod
[812]277    def directory(path):
278        """Loader factory for loading templates from a local directory.
279       
280        :param path: the path to the local directory containing the templates
281        :return: the loader function to load templates from the given directory
282        :rtype: ``function``
283        """
284        def _load_from_directory(filename):
285            filepath = os.path.join(path, filename)
[1190]286            fileobj = open(filepath, 'rb')
[830]287            mtime = os.path.getmtime(filepath)
288            def _uptodate():
289                return mtime == os.path.getmtime(filepath)
290            return filepath, filename, fileobj, _uptodate
[812]291        return _load_from_directory
292
[1031]293    @staticmethod
[812]294    def package(name, path):
295        """Loader factory for loading templates from egg package data.
296       
297        :param name: the name of the package containing the resources
298        :param path: the path inside the package data
299        :return: the loader function to load templates from the given package
300        :rtype: ``function``
301        """
302        from pkg_resources import resource_stream
303        def _load_from_package(filename):
304            filepath = os.path.join(path, filename)
[830]305            return filepath, filename, resource_stream(name, filepath), None
[812]306        return _load_from_package
307
[1031]308    @staticmethod
[812]309    def prefixed(**delegates):
310        """Factory for a load function that delegates to other loaders
311        depending on the prefix of the requested template path.
312       
313        The prefix is stripped from the filename when passing on the load
314        request to the delegate.
315       
316        >>> load = prefixed(
[814]317        ...     app1 = lambda filename: ('app1', filename, None, None),
318        ...     app2 = lambda filename: ('app2', filename, None, None)
[812]319        ... )
[1076]320        >>> print(load('app1/foo.html'))
[830]321        ('app1', 'app1/foo.html', None, None)
[1076]322        >>> print(load('app2/bar.html'))
[830]323        ('app2', 'app2/bar.html', None, None)
324       
[812]325        :param delegates: mapping of path prefixes to loader functions
326        :return: the loader function
327        :rtype: ``function``
328        """
329        def _dispatch_by_prefix(filename):
330            for prefix, delegate in delegates.items():
331                if filename.startswith(prefix):
332                    if isinstance(delegate, basestring):
[814]333                        delegate = directory(delegate)
[830]334                    filepath, _, fileobj, uptodate = delegate(
[814]335                        filename[len(prefix):].lstrip('/\\')
336                    )
[830]337                    return filepath, filename, fileobj, uptodate
[1077]338            raise TemplateNotFound(filename, list(delegates.keys()))
[812]339        return _dispatch_by_prefix
340
[1031]341
[812]342directory = TemplateLoader.directory
343package = TemplateLoader.package
344prefixed = TemplateLoader.prefixed
Note: See TracBrowser for help on using the repository browser.