OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 """ |
| 3 jinja2.loaders |
| 4 ~~~~~~~~~~~~~~ |
| 5 |
| 6 Jinja loader classes. |
| 7 |
| 8 :copyright: (c) 2010 by the Jinja Team. |
| 9 :license: BSD, see LICENSE for more details. |
| 10 """ |
| 11 import os |
| 12 import sys |
| 13 import weakref |
| 14 from types import ModuleType |
| 15 from os import path |
| 16 try: |
| 17 from hashlib import sha1 |
| 18 except ImportError: |
| 19 from sha import new as sha1 |
| 20 from jinja2.exceptions import TemplateNotFound |
| 21 from jinja2.utils import LRUCache, open_if_exists, internalcode |
| 22 |
| 23 |
| 24 def split_template_path(template): |
| 25 """Split a path into segments and perform a sanity check. If it detects |
| 26 '..' in the path it will raise a `TemplateNotFound` error. |
| 27 """ |
| 28 pieces = [] |
| 29 for piece in template.split('/'): |
| 30 if path.sep in piece \ |
| 31 or (path.altsep and path.altsep in piece) or \ |
| 32 piece == path.pardir: |
| 33 raise TemplateNotFound(template) |
| 34 elif piece and piece != '.': |
| 35 pieces.append(piece) |
| 36 return pieces |
| 37 |
| 38 |
| 39 class BaseLoader(object): |
| 40 """Baseclass for all loaders. Subclass this and override `get_source` to |
| 41 implement a custom loading mechanism. The environment provides a |
| 42 `get_template` method that calls the loader's `load` method to get the |
| 43 :class:`Template` object. |
| 44 |
| 45 A very basic example for a loader that looks up templates on the file |
| 46 system could look like this:: |
| 47 |
| 48 from jinja2 import BaseLoader, TemplateNotFound |
| 49 from os.path import join, exists, getmtime |
| 50 |
| 51 class MyLoader(BaseLoader): |
| 52 |
| 53 def __init__(self, path): |
| 54 self.path = path |
| 55 |
| 56 def get_source(self, environment, template): |
| 57 path = join(self.path, template) |
| 58 if not exists(path): |
| 59 raise TemplateNotFound(template) |
| 60 mtime = getmtime(path) |
| 61 with file(path) as f: |
| 62 source = f.read().decode('utf-8') |
| 63 return source, path, lambda: mtime == getmtime(path) |
| 64 """ |
| 65 |
| 66 #: if set to `False` it indicates that the loader cannot provide access |
| 67 #: to the source of templates. |
| 68 #: |
| 69 #: .. versionadded:: 2.4 |
| 70 has_source_access = True |
| 71 |
| 72 def get_source(self, environment, template): |
| 73 """Get the template source, filename and reload helper for a template. |
| 74 It's passed the environment and template name and has to return a |
| 75 tuple in the form ``(source, filename, uptodate)`` or raise a |
| 76 `TemplateNotFound` error if it can't locate the template. |
| 77 |
| 78 The source part of the returned tuple must be the source of the |
| 79 template as unicode string or a ASCII bytestring. The filename should |
| 80 be the name of the file on the filesystem if it was loaded from there, |
| 81 otherwise `None`. The filename is used by python for the tracebacks |
| 82 if no loader extension is used. |
| 83 |
| 84 The last item in the tuple is the `uptodate` function. If auto |
| 85 reloading is enabled it's always called to check if the template |
| 86 changed. No arguments are passed so the function must store the |
| 87 old state somewhere (for example in a closure). If it returns `False` |
| 88 the template will be reloaded. |
| 89 """ |
| 90 if not self.has_source_access: |
| 91 raise RuntimeError('%s cannot provide access to the source' % |
| 92 self.__class__.__name__) |
| 93 raise TemplateNotFound(template) |
| 94 |
| 95 def list_templates(self): |
| 96 """Iterates over all templates. If the loader does not support that |
| 97 it should raise a :exc:`TypeError` which is the default behavior. |
| 98 """ |
| 99 raise TypeError('this loader cannot iterate over all templates') |
| 100 |
| 101 @internalcode |
| 102 def load(self, environment, name, globals=None): |
| 103 """Loads a template. This method looks up the template in the cache |
| 104 or loads one by calling :meth:`get_source`. Subclasses should not |
| 105 override this method as loaders working on collections of other |
| 106 loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) |
| 107 will not call this method but `get_source` directly. |
| 108 """ |
| 109 code = None |
| 110 if globals is None: |
| 111 globals = {} |
| 112 |
| 113 # first we try to get the source for this template together |
| 114 # with the filename and the uptodate function. |
| 115 source, filename, uptodate = self.get_source(environment, name) |
| 116 |
| 117 # try to load the code from the bytecode cache if there is a |
| 118 # bytecode cache configured. |
| 119 bcc = environment.bytecode_cache |
| 120 if bcc is not None: |
| 121 bucket = bcc.get_bucket(environment, name, filename, source) |
| 122 code = bucket.code |
| 123 |
| 124 # if we don't have code so far (not cached, no longer up to |
| 125 # date) etc. we compile the template |
| 126 if code is None: |
| 127 code = environment.compile(source, name, filename) |
| 128 |
| 129 # if the bytecode cache is available and the bucket doesn't |
| 130 # have a code so far, we give the bucket the new code and put |
| 131 # it back to the bytecode cache. |
| 132 if bcc is not None and bucket.code is None: |
| 133 bucket.code = code |
| 134 bcc.set_bucket(bucket) |
| 135 |
| 136 return environment.template_class.from_code(environment, code, |
| 137 globals, uptodate) |
| 138 |
| 139 |
| 140 class FileSystemLoader(BaseLoader): |
| 141 """Loads templates from the file system. This loader can find templates |
| 142 in folders on the file system and is the preferred way to load them. |
| 143 |
| 144 The loader takes the path to the templates as string, or if multiple |
| 145 locations are wanted a list of them which is then looked up in the |
| 146 given order: |
| 147 |
| 148 >>> loader = FileSystemLoader('/path/to/templates') |
| 149 >>> loader = FileSystemLoader(['/path/to/templates', '/other/path']) |
| 150 |
| 151 Per default the template encoding is ``'utf-8'`` which can be changed |
| 152 by setting the `encoding` parameter to something else. |
| 153 """ |
| 154 |
| 155 def __init__(self, searchpath, encoding='utf-8'): |
| 156 if isinstance(searchpath, basestring): |
| 157 searchpath = [searchpath] |
| 158 self.searchpath = list(searchpath) |
| 159 self.encoding = encoding |
| 160 |
| 161 def get_source(self, environment, template): |
| 162 pieces = split_template_path(template) |
| 163 for searchpath in self.searchpath: |
| 164 filename = path.join(searchpath, *pieces) |
| 165 f = open_if_exists(filename) |
| 166 if f is None: |
| 167 continue |
| 168 try: |
| 169 contents = f.read().decode(self.encoding) |
| 170 finally: |
| 171 f.close() |
| 172 |
| 173 mtime = path.getmtime(filename) |
| 174 def uptodate(): |
| 175 try: |
| 176 return path.getmtime(filename) == mtime |
| 177 except OSError: |
| 178 return False |
| 179 return contents, filename, uptodate |
| 180 raise TemplateNotFound(template) |
| 181 |
| 182 def list_templates(self): |
| 183 found = set() |
| 184 for searchpath in self.searchpath: |
| 185 for dirpath, dirnames, filenames in os.walk(searchpath): |
| 186 for filename in filenames: |
| 187 template = os.path.join(dirpath, filename) \ |
| 188 [len(searchpath):].strip(os.path.sep) \ |
| 189 .replace(os.path.sep, '/') |
| 190 if template[:2] == './': |
| 191 template = template[2:] |
| 192 if template not in found: |
| 193 found.add(template) |
| 194 return sorted(found) |
| 195 |
| 196 |
| 197 class PackageLoader(BaseLoader): |
| 198 """Load templates from python eggs or packages. It is constructed with |
| 199 the name of the python package and the path to the templates in that |
| 200 package:: |
| 201 |
| 202 loader = PackageLoader('mypackage', 'views') |
| 203 |
| 204 If the package path is not given, ``'templates'`` is assumed. |
| 205 |
| 206 Per default the template encoding is ``'utf-8'`` which can be changed |
| 207 by setting the `encoding` parameter to something else. Due to the nature |
| 208 of eggs it's only possible to reload templates if the package was loaded |
| 209 from the file system and not a zip file. |
| 210 """ |
| 211 |
| 212 def __init__(self, package_name, package_path='templates', |
| 213 encoding='utf-8'): |
| 214 from pkg_resources import DefaultProvider, ResourceManager, \ |
| 215 get_provider |
| 216 provider = get_provider(package_name) |
| 217 self.encoding = encoding |
| 218 self.manager = ResourceManager() |
| 219 self.filesystem_bound = isinstance(provider, DefaultProvider) |
| 220 self.provider = provider |
| 221 self.package_path = package_path |
| 222 |
| 223 def get_source(self, environment, template): |
| 224 pieces = split_template_path(template) |
| 225 p = '/'.join((self.package_path,) + tuple(pieces)) |
| 226 if not self.provider.has_resource(p): |
| 227 raise TemplateNotFound(template) |
| 228 |
| 229 filename = uptodate = None |
| 230 if self.filesystem_bound: |
| 231 filename = self.provider.get_resource_filename(self.manager, p) |
| 232 mtime = path.getmtime(filename) |
| 233 def uptodate(): |
| 234 try: |
| 235 return path.getmtime(filename) == mtime |
| 236 except OSError: |
| 237 return False |
| 238 |
| 239 source = self.provider.get_resource_string(self.manager, p) |
| 240 return source.decode(self.encoding), filename, uptodate |
| 241 |
| 242 def list_templates(self): |
| 243 path = self.package_path |
| 244 if path[:2] == './': |
| 245 path = path[2:] |
| 246 elif path == '.': |
| 247 path = '' |
| 248 offset = len(path) |
| 249 results = [] |
| 250 def _walk(path): |
| 251 for filename in self.provider.resource_listdir(path): |
| 252 fullname = path + '/' + filename |
| 253 if self.provider.resource_isdir(fullname): |
| 254 _walk(fullname) |
| 255 else: |
| 256 results.append(fullname[offset:].lstrip('/')) |
| 257 _walk(path) |
| 258 results.sort() |
| 259 return results |
| 260 |
| 261 |
| 262 class DictLoader(BaseLoader): |
| 263 """Loads a template from a python dict. It's passed a dict of unicode |
| 264 strings bound to template names. This loader is useful for unittesting: |
| 265 |
| 266 >>> loader = DictLoader({'index.html': 'source here'}) |
| 267 |
| 268 Because auto reloading is rarely useful this is disabled per default. |
| 269 """ |
| 270 |
| 271 def __init__(self, mapping): |
| 272 self.mapping = mapping |
| 273 |
| 274 def get_source(self, environment, template): |
| 275 if template in self.mapping: |
| 276 source = self.mapping[template] |
| 277 return source, None, lambda: source != self.mapping.get(template) |
| 278 raise TemplateNotFound(template) |
| 279 |
| 280 def list_templates(self): |
| 281 return sorted(self.mapping) |
| 282 |
| 283 |
| 284 class FunctionLoader(BaseLoader): |
| 285 """A loader that is passed a function which does the loading. The |
| 286 function becomes the name of the template passed and has to return either |
| 287 an unicode string with the template source, a tuple in the form ``(source, |
| 288 filename, uptodatefunc)`` or `None` if the template does not exist. |
| 289 |
| 290 >>> def load_template(name): |
| 291 ... if name == 'index.html': |
| 292 ... return '...' |
| 293 ... |
| 294 >>> loader = FunctionLoader(load_template) |
| 295 |
| 296 The `uptodatefunc` is a function that is called if autoreload is enabled |
| 297 and has to return `True` if the template is still up to date. For more |
| 298 details have a look at :meth:`BaseLoader.get_source` which has the same |
| 299 return value. |
| 300 """ |
| 301 |
| 302 def __init__(self, load_func): |
| 303 self.load_func = load_func |
| 304 |
| 305 def get_source(self, environment, template): |
| 306 rv = self.load_func(template) |
| 307 if rv is None: |
| 308 raise TemplateNotFound(template) |
| 309 elif isinstance(rv, basestring): |
| 310 return rv, None, None |
| 311 return rv |
| 312 |
| 313 |
| 314 class PrefixLoader(BaseLoader): |
| 315 """A loader that is passed a dict of loaders where each loader is bound |
| 316 to a prefix. The prefix is delimited from the template by a slash per |
| 317 default, which can be changed by setting the `delimiter` argument to |
| 318 something else:: |
| 319 |
| 320 loader = PrefixLoader({ |
| 321 'app1': PackageLoader('mypackage.app1'), |
| 322 'app2': PackageLoader('mypackage.app2') |
| 323 }) |
| 324 |
| 325 By loading ``'app1/index.html'`` the file from the app1 package is loaded, |
| 326 by loading ``'app2/index.html'`` the file from the second. |
| 327 """ |
| 328 |
| 329 def __init__(self, mapping, delimiter='/'): |
| 330 self.mapping = mapping |
| 331 self.delimiter = delimiter |
| 332 |
| 333 def get_source(self, environment, template): |
| 334 try: |
| 335 prefix, name = template.split(self.delimiter, 1) |
| 336 loader = self.mapping[prefix] |
| 337 except (ValueError, KeyError): |
| 338 raise TemplateNotFound(template) |
| 339 try: |
| 340 return loader.get_source(environment, name) |
| 341 except TemplateNotFound: |
| 342 # re-raise the exception with the correct fileame here. |
| 343 # (the one that includes the prefix) |
| 344 raise TemplateNotFound(template) |
| 345 |
| 346 def list_templates(self): |
| 347 result = [] |
| 348 for prefix, loader in self.mapping.iteritems(): |
| 349 for template in loader.list_templates(): |
| 350 result.append(prefix + self.delimiter + template) |
| 351 return result |
| 352 |
| 353 |
| 354 class ChoiceLoader(BaseLoader): |
| 355 """This loader works like the `PrefixLoader` just that no prefix is |
| 356 specified. If a template could not be found by one loader the next one |
| 357 is tried. |
| 358 |
| 359 >>> loader = ChoiceLoader([ |
| 360 ... FileSystemLoader('/path/to/user/templates'), |
| 361 ... FileSystemLoader('/path/to/system/templates') |
| 362 ... ]) |
| 363 |
| 364 This is useful if you want to allow users to override builtin templates |
| 365 from a different location. |
| 366 """ |
| 367 |
| 368 def __init__(self, loaders): |
| 369 self.loaders = loaders |
| 370 |
| 371 def get_source(self, environment, template): |
| 372 for loader in self.loaders: |
| 373 try: |
| 374 return loader.get_source(environment, template) |
| 375 except TemplateNotFound: |
| 376 pass |
| 377 raise TemplateNotFound(template) |
| 378 |
| 379 def list_templates(self): |
| 380 found = set() |
| 381 for loader in self.loaders: |
| 382 found.update(loader.list_templates()) |
| 383 return sorted(found) |
| 384 |
| 385 |
| 386 class _TemplateModule(ModuleType): |
| 387 """Like a normal module but with support for weak references""" |
| 388 |
| 389 |
| 390 class ModuleLoader(BaseLoader): |
| 391 """This loader loads templates from precompiled templates. |
| 392 |
| 393 Example usage: |
| 394 |
| 395 >>> loader = ChoiceLoader([ |
| 396 ... ModuleLoader('/path/to/compiled/templates'), |
| 397 ... FileSystemLoader('/path/to/templates') |
| 398 ... ]) |
| 399 |
| 400 Templates can be precompiled with :meth:`Environment.compile_templates`. |
| 401 """ |
| 402 |
| 403 has_source_access = False |
| 404 |
| 405 def __init__(self, path): |
| 406 package_name = '_jinja2_module_templates_%x' % id(self) |
| 407 |
| 408 # create a fake module that looks for the templates in the |
| 409 # path given. |
| 410 mod = _TemplateModule(package_name) |
| 411 if isinstance(path, basestring): |
| 412 path = [path] |
| 413 else: |
| 414 path = list(path) |
| 415 mod.__path__ = path |
| 416 |
| 417 sys.modules[package_name] = weakref.proxy(mod, |
| 418 lambda x: sys.modules.pop(package_name, None)) |
| 419 |
| 420 # the only strong reference, the sys.modules entry is weak |
| 421 # so that the garbage collector can remove it once the |
| 422 # loader that created it goes out of business. |
| 423 self.module = mod |
| 424 self.package_name = package_name |
| 425 |
| 426 @staticmethod |
| 427 def get_template_key(name): |
| 428 return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest() |
| 429 |
| 430 @staticmethod |
| 431 def get_module_filename(name): |
| 432 return ModuleLoader.get_template_key(name) + '.py' |
| 433 |
| 434 @internalcode |
| 435 def load(self, environment, name, globals=None): |
| 436 key = self.get_template_key(name) |
| 437 module = '%s.%s' % (self.package_name, key) |
| 438 mod = getattr(self.module, module, None) |
| 439 if mod is None: |
| 440 try: |
| 441 mod = __import__(module, None, None, ['root']) |
| 442 except ImportError: |
| 443 raise TemplateNotFound(name) |
| 444 |
| 445 # remove the entry from sys.modules, we only want the attribute |
| 446 # on the module object we have stored on the loader. |
| 447 sys.modules.pop(module, None) |
| 448 |
| 449 return environment.template_class.from_module_dict( |
| 450 environment, mod.__dict__, globals) |
OLD | NEW |