| 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 | |
|---|
| 16 | import os |
|---|
| 17 | try: |
|---|
| 18 | import threading |
|---|
| 19 | except ImportError: |
|---|
| 20 | import dummy_threading as threading |
|---|
| 21 | |
|---|
| 22 | from genshi.template.base import TemplateError |
|---|
| 23 | from genshi.util import LRUCache |
|---|
| 24 | |
|---|
| 25 | __all__ = ['TemplateLoader', 'TemplateNotFound'] |
|---|
| 26 | __docformat__ = 'restructuredtext en' |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | class 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 | |
|---|
| 42 | class 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() |
|---|