| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2006-2010 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', 'directory', 'package', |
|---|
| 26 | 'prefixed'] |
|---|
| 27 | __docformat__ = 'restructuredtext en' |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | class 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 | |
|---|
| 43 | class 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 __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 | |
|---|
| 143 | def load(self, filename, relative_to=None, cls=None, encoding=None): |
|---|
| 144 | """Load the template with the given name. |
|---|
| 145 | |
|---|
| 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. |
|---|
| 149 | |
|---|
| 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. |
|---|
| 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` |
|---|
| 157 | object (unless the ``auto_reload`` option is enabled and the file was |
|---|
| 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 | |
|---|
| 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 |
|---|
| 165 | template is being loaded, or ``None`` if the |
|---|
| 166 | template is being loaded directly |
|---|
| 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 |
|---|
| 170 | :return: the loaded `Template` instance |
|---|
| 171 | :raises TemplateNotFound: if a template with the given name could not |
|---|
| 172 | be found |
|---|
| 173 | """ |
|---|
| 174 | if cls is None: |
|---|
| 175 | cls = self.default_class |
|---|
| 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)): |
|---|
| 182 | filename = os.path.join(os.path.dirname(relative_to), filename) |
|---|
| 183 | |
|---|
| 184 | filename = os.path.normpath(filename) |
|---|
| 185 | cachekey = filename |
|---|
| 186 | |
|---|
| 187 | self._lock.acquire() |
|---|
| 188 | try: |
|---|
| 189 | # First check the cache to avoid reparsing the same file |
|---|
| 190 | try: |
|---|
| 191 | tmpl = self._cache[cachekey] |
|---|
| 192 | if not self.auto_reload: |
|---|
| 193 | return tmpl |
|---|
| 194 | uptodate = self._uptodate[cachekey] |
|---|
| 195 | if uptodate is not None and uptodate(): |
|---|
| 196 | return tmpl |
|---|
| 197 | except (KeyError, OSError): |
|---|
| 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: |
|---|
| 212 | search_path = list(search_path) + [dirname] |
|---|
| 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 | |
|---|
| 219 | for loadfunc in search_path: |
|---|
| 220 | if isinstance(loadfunc, basestring): |
|---|
| 221 | loadfunc = directory(loadfunc) |
|---|
| 222 | try: |
|---|
| 223 | filepath, filename, fileobj, uptodate = loadfunc(filename) |
|---|
| 224 | except IOError: |
|---|
| 225 | continue |
|---|
| 226 | else: |
|---|
| 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, |
|---|
| 232 | # so that nested includes work properly without a |
|---|
| 233 | # search path |
|---|
| 234 | filename = filepath |
|---|
| 235 | tmpl = self._instantiate(cls, fileobj, filepath, |
|---|
| 236 | filename, encoding=encoding) |
|---|
| 237 | if self.callback: |
|---|
| 238 | self.callback(tmpl) |
|---|
| 239 | self._cache[cachekey] = tmpl |
|---|
| 240 | self._uptodate[cachekey] = uptodate |
|---|
| 241 | finally: |
|---|
| 242 | if hasattr(fileobj, 'close'): |
|---|
| 243 | fileobj.close() |
|---|
| 244 | return tmpl |
|---|
| 245 | |
|---|
| 246 | raise TemplateNotFound(filename, search_path) |
|---|
| 247 | |
|---|
| 248 | finally: |
|---|
| 249 | self._lock.release() |
|---|
| 250 | |
|---|
| 251 | def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 272 | return cls(fileobj, filepath=filepath, filename=filename, loader=self, |
|---|
| 273 | encoding=encoding, lookup=self.variable_lookup, |
|---|
| 274 | allow_exec=self.allow_exec) |
|---|
| 275 | |
|---|
| 276 | @staticmethod |
|---|
| 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) |
|---|
| 286 | fileobj = open(filepath, 'U') |
|---|
| 287 | mtime = os.path.getmtime(filepath) |
|---|
| 288 | def _uptodate(): |
|---|
| 289 | return mtime == os.path.getmtime(filepath) |
|---|
| 290 | return filepath, filename, fileobj, _uptodate |
|---|
| 291 | return _load_from_directory |
|---|
| 292 | |
|---|
| 293 | @staticmethod |
|---|
| 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) |
|---|
| 305 | return filepath, filename, resource_stream(name, filepath), None |
|---|
| 306 | return _load_from_package |
|---|
| 307 | |
|---|
| 308 | @staticmethod |
|---|
| 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( |
|---|
| 317 | ... app1 = lambda filename: ('app1', filename, None, None), |
|---|
| 318 | ... app2 = lambda filename: ('app2', filename, None, None) |
|---|
| 319 | ... ) |
|---|
| 320 | >>> print(load('app1/foo.html')) |
|---|
| 321 | ('app1', 'app1/foo.html', None, None) |
|---|
| 322 | >>> print(load('app2/bar.html')) |
|---|
| 323 | ('app2', 'app2/bar.html', None, None) |
|---|
| 324 | |
|---|
| 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): |
|---|
| 333 | delegate = directory(delegate) |
|---|
| 334 | filepath, _, fileobj, uptodate = delegate( |
|---|
| 335 | filename[len(prefix):].lstrip('/\\') |
|---|
| 336 | ) |
|---|
| 337 | return filepath, filename, fileobj, uptodate |
|---|
| 338 | raise TemplateNotFound(filename, list(delegates.keys())) |
|---|
| 339 | return _dispatch_by_prefix |
|---|
| 340 | |
|---|
| 341 | |
|---|
| 342 | directory = TemplateLoader.directory |
|---|
| 343 | package = TemplateLoader.package |
|---|
| 344 | prefixed = TemplateLoader.prefixed |
|---|