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