1"""API and implementations for loading templates from different data 2sources. 3""" 4import importlib.util 5import os 6import sys 7import weakref 8import zipimport 9from collections import abc 10from hashlib import sha1 11from importlib import import_module 12from types import ModuleType 13 14from .exceptions import TemplateNotFound 15from .utils import internalcode 16from .utils import open_if_exists 17 18 19def split_template_path(template): 20 """Split a path into segments and perform a sanity check. If it detects 21 '..' in the path it will raise a `TemplateNotFound` error. 22 """ 23 pieces = [] 24 for piece in template.split("/"): 25 if ( 26 os.path.sep in piece 27 or (os.path.altsep and os.path.altsep in piece) 28 or piece == os.path.pardir 29 ): 30 raise TemplateNotFound(template) 31 elif piece and piece != ".": 32 pieces.append(piece) 33 return pieces 34 35 36class BaseLoader: 37 """Baseclass for all loaders. Subclass this and override `get_source` to 38 implement a custom loading mechanism. The environment provides a 39 `get_template` method that calls the loader's `load` method to get the 40 :class:`Template` object. 41 42 A very basic example for a loader that looks up templates on the file 43 system could look like this:: 44 45 from jinja2 import BaseLoader, TemplateNotFound 46 from os.path import join, exists, getmtime 47 48 class MyLoader(BaseLoader): 49 50 def __init__(self, path): 51 self.path = path 52 53 def get_source(self, environment, template): 54 path = join(self.path, template) 55 if not exists(path): 56 raise TemplateNotFound(template) 57 mtime = getmtime(path) 58 with open(path) as f: 59 source = f.read() 60 return source, path, lambda: mtime == getmtime(path) 61 """ 62 63 #: if set to `False` it indicates that the loader cannot provide access 64 #: to the source of templates. 65 #: 66 #: .. versionadded:: 2.4 67 has_source_access = True 68 69 def get_source(self, environment, template): 70 """Get the template source, filename and reload helper for a template. 71 It's passed the environment and template name and has to return a 72 tuple in the form ``(source, filename, uptodate)`` or raise a 73 `TemplateNotFound` error if it can't locate the template. 74 75 The source part of the returned tuple must be the source of the 76 template as a string. The filename should be the name of the 77 file on the filesystem if it was loaded from there, otherwise 78 ``None``. The filename is used by Python for the tracebacks 79 if no loader extension is used. 80 81 The last item in the tuple is the `uptodate` function. If auto 82 reloading is enabled it's always called to check if the template 83 changed. No arguments are passed so the function must store the 84 old state somewhere (for example in a closure). If it returns `False` 85 the template will be reloaded. 86 """ 87 if not self.has_source_access: 88 raise RuntimeError( 89 f"{self.__class__.__name__} cannot provide access to the source" 90 ) 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( 135 environment, code, globals, uptodate 136 ) 137 138 139class FileSystemLoader(BaseLoader): 140 """Load templates from a directory in the file system. 141 142 The path can be relative or absolute. Relative paths are relative to 143 the current working directory. 144 145 .. code-block:: python 146 147 loader = FileSystemLoader("templates") 148 149 A list of paths can be given. The directories will be searched in 150 order, stopping at the first matching template. 151 152 .. code-block:: python 153 154 loader = FileSystemLoader(["/override/templates", "/default/templates"]) 155 156 :param searchpath: A path, or list of paths, to the directory that 157 contains the templates. 158 :param encoding: Use this encoding to read the text from template 159 files. 160 :param followlinks: Follow symbolic links in the path. 161 162 .. versionchanged:: 2.8 163 Added the ``followlinks`` parameter. 164 """ 165 166 def __init__(self, searchpath, encoding="utf-8", followlinks=False): 167 if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str): 168 searchpath = [searchpath] 169 170 self.searchpath = list(searchpath) 171 self.encoding = encoding 172 self.followlinks = followlinks 173 174 def get_source(self, environment, template): 175 pieces = split_template_path(template) 176 for searchpath in self.searchpath: 177 filename = os.path.join(searchpath, *pieces) 178 f = open_if_exists(filename) 179 if f is None: 180 continue 181 try: 182 contents = f.read().decode(self.encoding) 183 finally: 184 f.close() 185 186 mtime = os.path.getmtime(filename) 187 188 def uptodate(): 189 try: 190 return os.path.getmtime(filename) == mtime 191 except OSError: 192 return False 193 194 return contents, filename, uptodate 195 raise TemplateNotFound(template) 196 197 def list_templates(self): 198 found = set() 199 for searchpath in self.searchpath: 200 walk_dir = os.walk(searchpath, followlinks=self.followlinks) 201 for dirpath, _, filenames in walk_dir: 202 for filename in filenames: 203 template = ( 204 os.path.join(dirpath, filename)[len(searchpath) :] 205 .strip(os.path.sep) 206 .replace(os.path.sep, "/") 207 ) 208 if template[:2] == "./": 209 template = template[2:] 210 if template not in found: 211 found.add(template) 212 return sorted(found) 213 214 215class PackageLoader(BaseLoader): 216 """Load templates from a directory in a Python package. 217 218 :param package_name: Import name of the package that contains the 219 template directory. 220 :param package_path: Directory within the imported package that 221 contains the templates. 222 :param encoding: Encoding of template files. 223 224 The following example looks up templates in the ``pages`` directory 225 within the ``project.ui`` package. 226 227 .. code-block:: python 228 229 loader = PackageLoader("project.ui", "pages") 230 231 Only packages installed as directories (standard pip behavior) or 232 zip/egg files (less common) are supported. The Python API for 233 introspecting data in packages is too limited to support other 234 installation methods the way this loader requires. 235 236 There is limited support for :pep:`420` namespace packages. The 237 template directory is assumed to only be in one namespace 238 contributor. Zip files contributing to a namespace are not 239 supported. 240 241 .. versionchanged:: 3.0 242 No longer uses ``setuptools`` as a dependency. 243 244 .. versionchanged:: 3.0 245 Limited PEP 420 namespace package support. 246 """ 247 248 def __init__(self, package_name, package_path="templates", encoding="utf-8"): 249 if package_path == os.path.curdir: 250 package_path = "" 251 elif package_path[:2] == os.path.curdir + os.path.sep: 252 package_path = package_path[2:] 253 254 package_path = os.path.normpath(package_path).rstrip(os.path.sep) 255 self.package_path = package_path 256 self.package_name = package_name 257 self.encoding = encoding 258 259 # Make sure the package exists. This also makes namespace 260 # packages work, otherwise get_loader returns None. 261 import_module(package_name) 262 spec = importlib.util.find_spec(package_name) 263 self._loader = loader = spec.loader 264 self._archive = None 265 self._template_root = None 266 267 if isinstance(loader, zipimport.zipimporter): 268 self._archive = loader.archive 269 pkgdir = next(iter(spec.submodule_search_locations)) 270 self._template_root = os.path.join(pkgdir, package_path) 271 elif spec.submodule_search_locations: 272 # This will be one element for regular packages and multiple 273 # for namespace packages. 274 for root in spec.submodule_search_locations: 275 root = os.path.join(root, package_path) 276 277 if os.path.isdir(root): 278 self._template_root = root 279 break 280 281 if self._template_root is None: 282 raise ValueError( 283 f"The {package_name!r} package was not installed in a" 284 " way that PackageLoader understands." 285 ) 286 287 def get_source(self, environment, template): 288 p = os.path.join(self._template_root, *split_template_path(template)) 289 290 if self._archive is None: 291 # Package is a directory. 292 if not os.path.isfile(p): 293 raise TemplateNotFound(template) 294 295 with open(p, "rb") as f: 296 source = f.read() 297 298 mtime = os.path.getmtime(p) 299 300 def up_to_date(): 301 return os.path.isfile(p) and os.path.getmtime(p) == mtime 302 303 else: 304 # Package is a zip file. 305 try: 306 source = self._loader.get_data(p) 307 except OSError: 308 raise TemplateNotFound(template) 309 310 # Could use the zip's mtime for all template mtimes, but 311 # would need to safely reload the module if it's out of 312 # date, so just report it as always current. 313 up_to_date = None 314 315 return source.decode(self.encoding), p, up_to_date 316 317 def list_templates(self): 318 results = [] 319 320 if self._archive is None: 321 # Package is a directory. 322 offset = len(self._template_root) 323 324 for dirpath, _, filenames in os.walk(self._template_root): 325 dirpath = dirpath[offset:].lstrip(os.path.sep) 326 results.extend( 327 os.path.join(dirpath, name).replace(os.path.sep, "/") 328 for name in filenames 329 ) 330 else: 331 if not hasattr(self._loader, "_files"): 332 raise TypeError( 333 "This zip import does not have the required" 334 " metadata to list templates." 335 ) 336 337 # Package is a zip file. 338 prefix = ( 339 self._template_root[len(self._archive) :].lstrip(os.path.sep) 340 + os.path.sep 341 ) 342 offset = len(prefix) 343 344 for name in self._loader._files.keys(): 345 # Find names under the templates directory that aren't directories. 346 if name.startswith(prefix) and name[-1] != os.path.sep: 347 results.append(name[offset:].replace(os.path.sep, "/")) 348 349 results.sort() 350 return results 351 352 353class DictLoader(BaseLoader): 354 """Loads a template from a Python dict mapping template names to 355 template source. This loader is useful for unittesting: 356 357 >>> loader = DictLoader({'index.html': 'source here'}) 358 359 Because auto reloading is rarely useful this is disabled per default. 360 """ 361 362 def __init__(self, mapping): 363 self.mapping = mapping 364 365 def get_source(self, environment, template): 366 if template in self.mapping: 367 source = self.mapping[template] 368 return source, None, lambda: source == self.mapping.get(template) 369 raise TemplateNotFound(template) 370 371 def list_templates(self): 372 return sorted(self.mapping) 373 374 375class FunctionLoader(BaseLoader): 376 """A loader that is passed a function which does the loading. The 377 function receives the name of the template and has to return either 378 a string with the template source, a tuple in the form ``(source, 379 filename, uptodatefunc)`` or `None` if the template does not exist. 380 381 >>> def load_template(name): 382 ... if name == 'index.html': 383 ... return '...' 384 ... 385 >>> loader = FunctionLoader(load_template) 386 387 The `uptodatefunc` is a function that is called if autoreload is enabled 388 and has to return `True` if the template is still up to date. For more 389 details have a look at :meth:`BaseLoader.get_source` which has the same 390 return value. 391 """ 392 393 def __init__(self, load_func): 394 self.load_func = load_func 395 396 def get_source(self, environment, template): 397 rv = self.load_func(template) 398 if rv is None: 399 raise TemplateNotFound(template) 400 elif isinstance(rv, str): 401 return rv, None, None 402 return rv 403 404 405class PrefixLoader(BaseLoader): 406 """A loader that is passed a dict of loaders where each loader is bound 407 to a prefix. The prefix is delimited from the template by a slash per 408 default, which can be changed by setting the `delimiter` argument to 409 something else:: 410 411 loader = PrefixLoader({ 412 'app1': PackageLoader('mypackage.app1'), 413 'app2': PackageLoader('mypackage.app2') 414 }) 415 416 By loading ``'app1/index.html'`` the file from the app1 package is loaded, 417 by loading ``'app2/index.html'`` the file from the second. 418 """ 419 420 def __init__(self, mapping, delimiter="/"): 421 self.mapping = mapping 422 self.delimiter = delimiter 423 424 def get_loader(self, template): 425 try: 426 prefix, name = template.split(self.delimiter, 1) 427 loader = self.mapping[prefix] 428 except (ValueError, KeyError): 429 raise TemplateNotFound(template) 430 return loader, name 431 432 def get_source(self, environment, template): 433 loader, name = self.get_loader(template) 434 try: 435 return loader.get_source(environment, name) 436 except TemplateNotFound: 437 # re-raise the exception with the correct filename here. 438 # (the one that includes the prefix) 439 raise TemplateNotFound(template) 440 441 @internalcode 442 def load(self, environment, name, globals=None): 443 loader, local_name = self.get_loader(name) 444 try: 445 return loader.load(environment, local_name, globals) 446 except TemplateNotFound: 447 # re-raise the exception with the correct filename here. 448 # (the one that includes the prefix) 449 raise TemplateNotFound(name) 450 451 def list_templates(self): 452 result = [] 453 for prefix, loader in self.mapping.items(): 454 for template in loader.list_templates(): 455 result.append(prefix + self.delimiter + template) 456 return result 457 458 459class ChoiceLoader(BaseLoader): 460 """This loader works like the `PrefixLoader` just that no prefix is 461 specified. If a template could not be found by one loader the next one 462 is tried. 463 464 >>> loader = ChoiceLoader([ 465 ... FileSystemLoader('/path/to/user/templates'), 466 ... FileSystemLoader('/path/to/system/templates') 467 ... ]) 468 469 This is useful if you want to allow users to override builtin templates 470 from a different location. 471 """ 472 473 def __init__(self, loaders): 474 self.loaders = loaders 475 476 def get_source(self, environment, template): 477 for loader in self.loaders: 478 try: 479 return loader.get_source(environment, template) 480 except TemplateNotFound: 481 pass 482 raise TemplateNotFound(template) 483 484 @internalcode 485 def load(self, environment, name, globals=None): 486 for loader in self.loaders: 487 try: 488 return loader.load(environment, name, globals) 489 except TemplateNotFound: 490 pass 491 raise TemplateNotFound(name) 492 493 def list_templates(self): 494 found = set() 495 for loader in self.loaders: 496 found.update(loader.list_templates()) 497 return sorted(found) 498 499 500class _TemplateModule(ModuleType): 501 """Like a normal module but with support for weak references""" 502 503 504class ModuleLoader(BaseLoader): 505 """This loader loads templates from precompiled templates. 506 507 Example usage: 508 509 >>> loader = ChoiceLoader([ 510 ... ModuleLoader('/path/to/compiled/templates'), 511 ... FileSystemLoader('/path/to/templates') 512 ... ]) 513 514 Templates can be precompiled with :meth:`Environment.compile_templates`. 515 """ 516 517 has_source_access = False 518 519 def __init__(self, path): 520 package_name = f"_jinja2_module_templates_{id(self):x}" 521 522 # create a fake module that looks for the templates in the 523 # path given. 524 mod = _TemplateModule(package_name) 525 526 if not isinstance(path, abc.Iterable) or isinstance(path, str): 527 path = [path] 528 529 mod.__path__ = [os.fspath(p) for p in path] 530 531 sys.modules[package_name] = weakref.proxy( 532 mod, lambda x: sys.modules.pop(package_name, None) 533 ) 534 535 # the only strong reference, the sys.modules entry is weak 536 # so that the garbage collector can remove it once the 537 # loader that created it goes out of business. 538 self.module = mod 539 self.package_name = package_name 540 541 @staticmethod 542 def get_template_key(name): 543 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() 544 545 @staticmethod 546 def get_module_filename(name): 547 return ModuleLoader.get_template_key(name) + ".py" 548 549 @internalcode 550 def load(self, environment, name, globals=None): 551 key = self.get_template_key(name) 552 module = f"{self.package_name}.{key}" 553 mod = getattr(self.module, module, None) 554 if mod is None: 555 try: 556 mod = __import__(module, None, None, ["root"]) 557 except ImportError: 558 raise TemplateNotFound(name) 559 560 # remove the entry from sys.modules, we only want the attribute 561 # on the module object we have stored on the loader. 562 sys.modules.pop(module, None) 563 564 return environment.template_class.from_module_dict( 565 environment, mod.__dict__, globals 566 ) 567