1# -*- coding: utf-8 -*- 2""" 3 jinja2.loaders 4 ~~~~~~~~~~~~~~ 5 6 Jinja loader classes. 7 8 :copyright: (c) 2017 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 To follow symbolic links, set the *followlinks* parameter to ``True``:: 153 154 >>> loader = FileSystemLoader('/path/to/templates', followlinks=True) 155 156 .. versionchanged:: 2.8+ 157 The *followlinks* parameter was added. 158 """ 159 160 def __init__(self, searchpath, encoding='utf-8', followlinks=False): 161 if isinstance(searchpath, string_types): 162 searchpath = [searchpath] 163 self.searchpath = list(searchpath) 164 self.encoding = encoding 165 self.followlinks = followlinks 166 167 def get_source(self, environment, template): 168 pieces = split_template_path(template) 169 for searchpath in self.searchpath: 170 filename = path.join(searchpath, *pieces) 171 f = open_if_exists(filename) 172 if f is None: 173 continue 174 try: 175 contents = f.read().decode(self.encoding) 176 finally: 177 f.close() 178 179 mtime = path.getmtime(filename) 180 181 def uptodate(): 182 try: 183 return path.getmtime(filename) == mtime 184 except OSError: 185 return False 186 return contents, filename, uptodate 187 raise TemplateNotFound(template) 188 189 def list_templates(self): 190 found = set() 191 for searchpath in self.searchpath: 192 walk_dir = os.walk(searchpath, followlinks=self.followlinks) 193 for dirpath, dirnames, filenames in walk_dir: 194 for filename in filenames: 195 template = os.path.join(dirpath, filename) \ 196 [len(searchpath):].strip(os.path.sep) \ 197 .replace(os.path.sep, '/') 198 if template[:2] == './': 199 template = template[2:] 200 if template not in found: 201 found.add(template) 202 return sorted(found) 203 204 205class PackageLoader(BaseLoader): 206 """Load templates from python eggs or packages. It is constructed with 207 the name of the python package and the path to the templates in that 208 package:: 209 210 loader = PackageLoader('mypackage', 'views') 211 212 If the package path is not given, ``'templates'`` is assumed. 213 214 Per default the template encoding is ``'utf-8'`` which can be changed 215 by setting the `encoding` parameter to something else. Due to the nature 216 of eggs it's only possible to reload templates if the package was loaded 217 from the file system and not a zip file. 218 """ 219 220 def __init__(self, package_name, package_path='templates', 221 encoding='utf-8'): 222 from pkg_resources import DefaultProvider, ResourceManager, \ 223 get_provider 224 provider = get_provider(package_name) 225 self.encoding = encoding 226 self.manager = ResourceManager() 227 self.filesystem_bound = isinstance(provider, DefaultProvider) 228 self.provider = provider 229 self.package_path = package_path 230 231 def get_source(self, environment, template): 232 pieces = split_template_path(template) 233 p = '/'.join((self.package_path,) + tuple(pieces)) 234 if not self.provider.has_resource(p): 235 raise TemplateNotFound(template) 236 237 filename = uptodate = None 238 if self.filesystem_bound: 239 filename = self.provider.get_resource_filename(self.manager, p) 240 mtime = path.getmtime(filename) 241 def uptodate(): 242 try: 243 return path.getmtime(filename) == mtime 244 except OSError: 245 return False 246 247 source = self.provider.get_resource_string(self.manager, p) 248 return source.decode(self.encoding), filename, uptodate 249 250 def list_templates(self): 251 path = self.package_path 252 if path[:2] == './': 253 path = path[2:] 254 elif path == '.': 255 path = '' 256 offset = len(path) 257 results = [] 258 def _walk(path): 259 for filename in self.provider.resource_listdir(path): 260 fullname = path + '/' + filename 261 if self.provider.resource_isdir(fullname): 262 _walk(fullname) 263 else: 264 results.append(fullname[offset:].lstrip('/')) 265 _walk(path) 266 results.sort() 267 return results 268 269 270class DictLoader(BaseLoader): 271 """Loads a template from a python dict. It's passed a dict of unicode 272 strings bound to template names. This loader is useful for unittesting: 273 274 >>> loader = DictLoader({'index.html': 'source here'}) 275 276 Because auto reloading is rarely useful this is disabled per default. 277 """ 278 279 def __init__(self, mapping): 280 self.mapping = mapping 281 282 def get_source(self, environment, template): 283 if template in self.mapping: 284 source = self.mapping[template] 285 return source, None, lambda: source == self.mapping.get(template) 286 raise TemplateNotFound(template) 287 288 def list_templates(self): 289 return sorted(self.mapping) 290 291 292class FunctionLoader(BaseLoader): 293 """A loader that is passed a function which does the loading. The 294 function receives the name of the template and has to return either 295 an unicode string with the template source, a tuple in the form ``(source, 296 filename, uptodatefunc)`` or `None` if the template does not exist. 297 298 >>> def load_template(name): 299 ... if name == 'index.html': 300 ... return '...' 301 ... 302 >>> loader = FunctionLoader(load_template) 303 304 The `uptodatefunc` is a function that is called if autoreload is enabled 305 and has to return `True` if the template is still up to date. For more 306 details have a look at :meth:`BaseLoader.get_source` which has the same 307 return value. 308 """ 309 310 def __init__(self, load_func): 311 self.load_func = load_func 312 313 def get_source(self, environment, template): 314 rv = self.load_func(template) 315 if rv is None: 316 raise TemplateNotFound(template) 317 elif isinstance(rv, string_types): 318 return rv, None, None 319 return rv 320 321 322class PrefixLoader(BaseLoader): 323 """A loader that is passed a dict of loaders where each loader is bound 324 to a prefix. The prefix is delimited from the template by a slash per 325 default, which can be changed by setting the `delimiter` argument to 326 something else:: 327 328 loader = PrefixLoader({ 329 'app1': PackageLoader('mypackage.app1'), 330 'app2': PackageLoader('mypackage.app2') 331 }) 332 333 By loading ``'app1/index.html'`` the file from the app1 package is loaded, 334 by loading ``'app2/index.html'`` the file from the second. 335 """ 336 337 def __init__(self, mapping, delimiter='/'): 338 self.mapping = mapping 339 self.delimiter = delimiter 340 341 def get_loader(self, template): 342 try: 343 prefix, name = template.split(self.delimiter, 1) 344 loader = self.mapping[prefix] 345 except (ValueError, KeyError): 346 raise TemplateNotFound(template) 347 return loader, name 348 349 def get_source(self, environment, template): 350 loader, name = self.get_loader(template) 351 try: 352 return loader.get_source(environment, name) 353 except TemplateNotFound: 354 # re-raise the exception with the correct filename here. 355 # (the one that includes the prefix) 356 raise TemplateNotFound(template) 357 358 @internalcode 359 def load(self, environment, name, globals=None): 360 loader, local_name = self.get_loader(name) 361 try: 362 return loader.load(environment, local_name, globals) 363 except TemplateNotFound: 364 # re-raise the exception with the correct filename here. 365 # (the one that includes the prefix) 366 raise TemplateNotFound(name) 367 368 def list_templates(self): 369 result = [] 370 for prefix, loader in iteritems(self.mapping): 371 for template in loader.list_templates(): 372 result.append(prefix + self.delimiter + template) 373 return result 374 375 376class ChoiceLoader(BaseLoader): 377 """This loader works like the `PrefixLoader` just that no prefix is 378 specified. If a template could not be found by one loader the next one 379 is tried. 380 381 >>> loader = ChoiceLoader([ 382 ... FileSystemLoader('/path/to/user/templates'), 383 ... FileSystemLoader('/path/to/system/templates') 384 ... ]) 385 386 This is useful if you want to allow users to override builtin templates 387 from a different location. 388 """ 389 390 def __init__(self, loaders): 391 self.loaders = loaders 392 393 def get_source(self, environment, template): 394 for loader in self.loaders: 395 try: 396 return loader.get_source(environment, template) 397 except TemplateNotFound: 398 pass 399 raise TemplateNotFound(template) 400 401 @internalcode 402 def load(self, environment, name, globals=None): 403 for loader in self.loaders: 404 try: 405 return loader.load(environment, name, globals) 406 except TemplateNotFound: 407 pass 408 raise TemplateNotFound(name) 409 410 def list_templates(self): 411 found = set() 412 for loader in self.loaders: 413 found.update(loader.list_templates()) 414 return sorted(found) 415 416 417class _TemplateModule(ModuleType): 418 """Like a normal module but with support for weak references""" 419 420 421class ModuleLoader(BaseLoader): 422 """This loader loads templates from precompiled templates. 423 424 Example usage: 425 426 >>> loader = ChoiceLoader([ 427 ... ModuleLoader('/path/to/compiled/templates'), 428 ... FileSystemLoader('/path/to/templates') 429 ... ]) 430 431 Templates can be precompiled with :meth:`Environment.compile_templates`. 432 """ 433 434 has_source_access = False 435 436 def __init__(self, path): 437 package_name = '_jinja2_module_templates_%x' % id(self) 438 439 # create a fake module that looks for the templates in the 440 # path given. 441 mod = _TemplateModule(package_name) 442 if isinstance(path, string_types): 443 path = [path] 444 else: 445 path = list(path) 446 mod.__path__ = path 447 448 sys.modules[package_name] = weakref.proxy(mod, 449 lambda x: sys.modules.pop(package_name, None)) 450 451 # the only strong reference, the sys.modules entry is weak 452 # so that the garbage collector can remove it once the 453 # loader that created it goes out of business. 454 self.module = mod 455 self.package_name = package_name 456 457 @staticmethod 458 def get_template_key(name): 459 return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest() 460 461 @staticmethod 462 def get_module_filename(name): 463 return ModuleLoader.get_template_key(name) + '.py' 464 465 @internalcode 466 def load(self, environment, name, globals=None): 467 key = self.get_template_key(name) 468 module = '%s.%s' % (self.package_name, key) 469 mod = getattr(self.module, module, None) 470 if mod is None: 471 try: 472 mod = __import__(module, None, None, ['root']) 473 except ImportError: 474 raise TemplateNotFound(name) 475 476 # remove the entry from sys.modules, we only want the attribute 477 # on the module object we have stored on the loader. 478 sys.modules.pop(module, None) 479 480 return environment.template_class.from_module_dict( 481 environment, mod.__dict__, globals) 482