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