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