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""" 11import os 12import sys 13import weakref 14from types import ModuleType 15from os import path 16try: 17 from hashlib import sha1 18except ImportError: 19 from sha import new as sha1 20from jinja2.exceptions import TemplateNotFound 21from jinja2.utils import LRUCache, open_if_exists, internalcode 22 23 24def 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 39class 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 140class 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 197class 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 for item in _walk(fullname): 255 results.append(item) 256 else: 257 results.append(fullname[offset:].lstrip('/')) 258 _walk(path) 259 results.sort() 260 return results 261 262 263class DictLoader(BaseLoader): 264 """Loads a template from a python dict. It's passed a dict of unicode 265 strings bound to template names. This loader is useful for unittesting: 266 267 >>> loader = DictLoader({'index.html': 'source here'}) 268 269 Because auto reloading is rarely useful this is disabled per default. 270 """ 271 272 def __init__(self, mapping): 273 self.mapping = mapping 274 275 def get_source(self, environment, template): 276 if template in self.mapping: 277 source = self.mapping[template] 278 return source, None, lambda: source != self.mapping.get(template) 279 raise TemplateNotFound(template) 280 281 def list_templates(self): 282 return sorted(self.mapping) 283 284 285class FunctionLoader(BaseLoader): 286 """A loader that is passed a function which does the loading. The 287 function becomes the name of the template passed and has to return either 288 an unicode string with the template source, a tuple in the form ``(source, 289 filename, uptodatefunc)`` or `None` if the template does not exist. 290 291 >>> def load_template(name): 292 ... if name == 'index.html': 293 ... return '...' 294 ... 295 >>> loader = FunctionLoader(load_template) 296 297 The `uptodatefunc` is a function that is called if autoreload is enabled 298 and has to return `True` if the template is still up to date. For more 299 details have a look at :meth:`BaseLoader.get_source` which has the same 300 return value. 301 """ 302 303 def __init__(self, load_func): 304 self.load_func = load_func 305 306 def get_source(self, environment, template): 307 rv = self.load_func(template) 308 if rv is None: 309 raise TemplateNotFound(template) 310 elif isinstance(rv, basestring): 311 return rv, None, None 312 return rv 313 314 315class PrefixLoader(BaseLoader): 316 """A loader that is passed a dict of loaders where each loader is bound 317 to a prefix. The prefix is delimited from the template by a slash per 318 default, which can be changed by setting the `delimiter` argument to 319 something else:: 320 321 loader = PrefixLoader({ 322 'app1': PackageLoader('mypackage.app1'), 323 'app2': PackageLoader('mypackage.app2') 324 }) 325 326 By loading ``'app1/index.html'`` the file from the app1 package is loaded, 327 by loading ``'app2/index.html'`` the file from the second. 328 """ 329 330 def __init__(self, mapping, delimiter='/'): 331 self.mapping = mapping 332 self.delimiter = delimiter 333 334 def get_source(self, environment, template): 335 try: 336 prefix, name = template.split(self.delimiter, 1) 337 loader = self.mapping[prefix] 338 except (ValueError, KeyError): 339 raise TemplateNotFound(template) 340 try: 341 return loader.get_source(environment, name) 342 except TemplateNotFound: 343 # re-raise the exception with the correct fileame here. 344 # (the one that includes the prefix) 345 raise TemplateNotFound(template) 346 347 def list_templates(self): 348 result = [] 349 for prefix, loader in self.mapping.iteritems(): 350 for template in loader.list_templates(): 351 result.append(prefix + self.delimiter + template) 352 return result 353 354 355class ChoiceLoader(BaseLoader): 356 """This loader works like the `PrefixLoader` just that no prefix is 357 specified. If a template could not be found by one loader the next one 358 is tried. 359 360 >>> loader = ChoiceLoader([ 361 ... FileSystemLoader('/path/to/user/templates'), 362 ... FileSystemLoader('/path/to/system/templates') 363 ... ]) 364 365 This is useful if you want to allow users to override builtin templates 366 from a different location. 367 """ 368 369 def __init__(self, loaders): 370 self.loaders = loaders 371 372 def get_source(self, environment, template): 373 for loader in self.loaders: 374 try: 375 return loader.get_source(environment, template) 376 except TemplateNotFound: 377 pass 378 raise TemplateNotFound(template) 379 380 def list_templates(self): 381 found = set() 382 for loader in self.loaders: 383 found.update(loader.list_templates()) 384 return sorted(found) 385 386 387class _TemplateModule(ModuleType): 388 """Like a normal module but with support for weak references""" 389 390 391class ModuleLoader(BaseLoader): 392 """This loader loads templates from precompiled templates. 393 394 Example usage: 395 396 >>> loader = ChoiceLoader([ 397 ... ModuleLoader('/path/to/compiled/templates'), 398 ... FileSystemLoader('/path/to/templates') 399 ... ]) 400 """ 401 402 has_source_access = False 403 404 def __init__(self, path): 405 package_name = '_jinja2_module_templates_%x' % id(self) 406 407 # create a fake module that looks for the templates in the 408 # path given. 409 mod = _TemplateModule(package_name) 410 if isinstance(path, basestring): 411 path = [path] 412 else: 413 path = list(path) 414 mod.__path__ = path 415 416 sys.modules[package_name] = weakref.proxy(mod, 417 lambda x: sys.modules.pop(package_name, None)) 418 419 # the only strong reference, the sys.modules entry is weak 420 # so that the garbage collector can remove it once the 421 # loader that created it goes out of business. 422 self.module = mod 423 self.package_name = package_name 424 425 @staticmethod 426 def get_template_key(name): 427 return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest() 428 429 @staticmethod 430 def get_module_filename(name): 431 return ModuleLoader.get_template_key(name) + '.py' 432 433 @internalcode 434 def load(self, environment, name, globals=None): 435 key = self.get_template_key(name) 436 module = '%s.%s' % (self.package_name, key) 437 mod = getattr(self.module, module, None) 438 if mod is None: 439 try: 440 mod = __import__(module, None, None, ['root']) 441 except ImportError: 442 raise TemplateNotFound(name) 443 444 # remove the entry from sys.modules, we only want the attribute 445 # on the module object we have stored on the loader. 446 sys.modules.pop(module, None) 447 448 return environment.template_class.from_module_dict( 449 environment, mod.__dict__, globals) 450