| """API and implementations for loading templates from different data |
| sources. |
| """ |
| import importlib.util |
| import os |
| import posixpath |
| import sys |
| import typing as t |
| import weakref |
| import zipimport |
| from collections import abc |
| from hashlib import sha1 |
| from importlib import import_module |
| from types import ModuleType |
| |
| from .exceptions import TemplateNotFound |
| from .utils import internalcode |
| from .utils import open_if_exists |
| |
| if t.TYPE_CHECKING: |
| from .environment import Environment |
| from .environment import Template |
| |
| |
| def split_template_path(template: str) -> t.List[str]: |
| """Split a path into segments and perform a sanity check. If it detects |
| '..' in the path it will raise a `TemplateNotFound` error. |
| """ |
| pieces = [] |
| for piece in template.split("/"): |
| if ( |
| os.path.sep in piece |
| or (os.path.altsep and os.path.altsep in piece) |
| or piece == os.path.pardir |
| ): |
| raise TemplateNotFound(template) |
| elif piece and piece != ".": |
| pieces.append(piece) |
| return pieces |
| |
| |
| class BaseLoader: |
| """Baseclass for all loaders. Subclass this and override `get_source` to |
| implement a custom loading mechanism. The environment provides a |
| `get_template` method that calls the loader's `load` method to get the |
| :class:`Template` object. |
| |
| A very basic example for a loader that looks up templates on the file |
| system could look like this:: |
| |
| from jinja2 import BaseLoader, TemplateNotFound |
| from os.path import join, exists, getmtime |
| |
| class MyLoader(BaseLoader): |
| |
| def __init__(self, path): |
| self.path = path |
| |
| def get_source(self, environment, template): |
| path = join(self.path, template) |
| if not exists(path): |
| raise TemplateNotFound(template) |
| mtime = getmtime(path) |
| with open(path) as f: |
| source = f.read() |
| return source, path, lambda: mtime == getmtime(path) |
| """ |
| |
| #: if set to `False` it indicates that the loader cannot provide access |
| #: to the source of templates. |
| #: |
| #: .. versionadded:: 2.4 |
| has_source_access = True |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: |
| """Get the template source, filename and reload helper for a template. |
| It's passed the environment and template name and has to return a |
| tuple in the form ``(source, filename, uptodate)`` or raise a |
| `TemplateNotFound` error if it can't locate the template. |
| |
| The source part of the returned tuple must be the source of the |
| template as a string. The filename should be the name of the |
| file on the filesystem if it was loaded from there, otherwise |
| ``None``. The filename is used by Python for the tracebacks |
| if no loader extension is used. |
| |
| The last item in the tuple is the `uptodate` function. If auto |
| reloading is enabled it's always called to check if the template |
| changed. No arguments are passed so the function must store the |
| old state somewhere (for example in a closure). If it returns `False` |
| the template will be reloaded. |
| """ |
| if not self.has_source_access: |
| raise RuntimeError( |
| f"{type(self).__name__} cannot provide access to the source" |
| ) |
| raise TemplateNotFound(template) |
| |
| def list_templates(self) -> t.List[str]: |
| """Iterates over all templates. If the loader does not support that |
| it should raise a :exc:`TypeError` which is the default behavior. |
| """ |
| raise TypeError("this loader cannot iterate over all templates") |
| |
| @internalcode |
| def load( |
| self, |
| environment: "Environment", |
| name: str, |
| globals: t.Optional[t.MutableMapping[str, t.Any]] = None, |
| ) -> "Template": |
| """Loads a template. This method looks up the template in the cache |
| or loads one by calling :meth:`get_source`. Subclasses should not |
| override this method as loaders working on collections of other |
| loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) |
| will not call this method but `get_source` directly. |
| """ |
| code = None |
| if globals is None: |
| globals = {} |
| |
| # first we try to get the source for this template together |
| # with the filename and the uptodate function. |
| source, filename, uptodate = self.get_source(environment, name) |
| |
| # try to load the code from the bytecode cache if there is a |
| # bytecode cache configured. |
| bcc = environment.bytecode_cache |
| if bcc is not None: |
| bucket = bcc.get_bucket(environment, name, filename, source) |
| code = bucket.code |
| |
| # if we don't have code so far (not cached, no longer up to |
| # date) etc. we compile the template |
| if code is None: |
| code = environment.compile(source, name, filename) |
| |
| # if the bytecode cache is available and the bucket doesn't |
| # have a code so far, we give the bucket the new code and put |
| # it back to the bytecode cache. |
| if bcc is not None and bucket.code is None: |
| bucket.code = code |
| bcc.set_bucket(bucket) |
| |
| return environment.template_class.from_code( |
| environment, code, globals, uptodate |
| ) |
| |
| |
| class FileSystemLoader(BaseLoader): |
| """Load templates from a directory in the file system. |
| |
| The path can be relative or absolute. Relative paths are relative to |
| the current working directory. |
| |
| .. code-block:: python |
| |
| loader = FileSystemLoader("templates") |
| |
| A list of paths can be given. The directories will be searched in |
| order, stopping at the first matching template. |
| |
| .. code-block:: python |
| |
| loader = FileSystemLoader(["/override/templates", "/default/templates"]) |
| |
| :param searchpath: A path, or list of paths, to the directory that |
| contains the templates. |
| :param encoding: Use this encoding to read the text from template |
| files. |
| :param followlinks: Follow symbolic links in the path. |
| |
| .. versionchanged:: 2.8 |
| Added the ``followlinks`` parameter. |
| """ |
| |
| def __init__( |
| self, |
| searchpath: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]], |
| encoding: str = "utf-8", |
| followlinks: bool = False, |
| ) -> None: |
| if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str): |
| searchpath = [searchpath] |
| |
| self.searchpath = [os.fspath(p) for p in searchpath] |
| self.encoding = encoding |
| self.followlinks = followlinks |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, str, t.Callable[[], bool]]: |
| pieces = split_template_path(template) |
| for searchpath in self.searchpath: |
| # Use posixpath even on Windows to avoid "drive:" or UNC |
| # segments breaking out of the search directory. |
| filename = posixpath.join(searchpath, *pieces) |
| f = open_if_exists(filename) |
| if f is None: |
| continue |
| try: |
| contents = f.read().decode(self.encoding) |
| finally: |
| f.close() |
| |
| mtime = os.path.getmtime(filename) |
| |
| def uptodate() -> bool: |
| try: |
| return os.path.getmtime(filename) == mtime |
| except OSError: |
| return False |
| |
| # Use normpath to convert Windows altsep to sep. |
| return contents, os.path.normpath(filename), uptodate |
| raise TemplateNotFound(template) |
| |
| def list_templates(self) -> t.List[str]: |
| found = set() |
| for searchpath in self.searchpath: |
| walk_dir = os.walk(searchpath, followlinks=self.followlinks) |
| for dirpath, _, filenames in walk_dir: |
| for filename in filenames: |
| template = ( |
| os.path.join(dirpath, filename)[len(searchpath) :] |
| .strip(os.path.sep) |
| .replace(os.path.sep, "/") |
| ) |
| if template[:2] == "./": |
| template = template[2:] |
| if template not in found: |
| found.add(template) |
| return sorted(found) |
| |
| |
| class PackageLoader(BaseLoader): |
| """Load templates from a directory in a Python package. |
| |
| :param package_name: Import name of the package that contains the |
| template directory. |
| :param package_path: Directory within the imported package that |
| contains the templates. |
| :param encoding: Encoding of template files. |
| |
| The following example looks up templates in the ``pages`` directory |
| within the ``project.ui`` package. |
| |
| .. code-block:: python |
| |
| loader = PackageLoader("project.ui", "pages") |
| |
| Only packages installed as directories (standard pip behavior) or |
| zip/egg files (less common) are supported. The Python API for |
| introspecting data in packages is too limited to support other |
| installation methods the way this loader requires. |
| |
| There is limited support for :pep:`420` namespace packages. The |
| template directory is assumed to only be in one namespace |
| contributor. Zip files contributing to a namespace are not |
| supported. |
| |
| .. versionchanged:: 3.0 |
| No longer uses ``setuptools`` as a dependency. |
| |
| .. versionchanged:: 3.0 |
| Limited PEP 420 namespace package support. |
| """ |
| |
| def __init__( |
| self, |
| package_name: str, |
| package_path: "str" = "templates", |
| encoding: str = "utf-8", |
| ) -> None: |
| package_path = os.path.normpath(package_path).rstrip(os.path.sep) |
| |
| # normpath preserves ".", which isn't valid in zip paths. |
| if package_path == os.path.curdir: |
| package_path = "" |
| elif package_path[:2] == os.path.curdir + os.path.sep: |
| package_path = package_path[2:] |
| |
| self.package_path = package_path |
| self.package_name = package_name |
| self.encoding = encoding |
| |
| # Make sure the package exists. This also makes namespace |
| # packages work, otherwise get_loader returns None. |
| import_module(package_name) |
| spec = importlib.util.find_spec(package_name) |
| assert spec is not None, "An import spec was not found for the package." |
| loader = spec.loader |
| assert loader is not None, "A loader was not found for the package." |
| self._loader = loader |
| self._archive = None |
| template_root = None |
| |
| if isinstance(loader, zipimport.zipimporter): |
| self._archive = loader.archive |
| pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore |
| template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep) |
| else: |
| roots: t.List[str] = [] |
| |
| # One element for regular packages, multiple for namespace |
| # packages, or None for single module file. |
| if spec.submodule_search_locations: |
| roots.extend(spec.submodule_search_locations) |
| # A single module file, use the parent directory instead. |
| elif spec.origin is not None: |
| roots.append(os.path.dirname(spec.origin)) |
| |
| for root in roots: |
| root = os.path.join(root, package_path) |
| |
| if os.path.isdir(root): |
| template_root = root |
| break |
| |
| if template_root is None: |
| raise ValueError( |
| f"The {package_name!r} package was not installed in a" |
| " way that PackageLoader understands." |
| ) |
| |
| self._template_root = template_root |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]: |
| # Use posixpath even on Windows to avoid "drive:" or UNC |
| # segments breaking out of the search directory. Use normpath to |
| # convert Windows altsep to sep. |
| p = os.path.normpath( |
| posixpath.join(self._template_root, *split_template_path(template)) |
| ) |
| up_to_date: t.Optional[t.Callable[[], bool]] |
| |
| if self._archive is None: |
| # Package is a directory. |
| if not os.path.isfile(p): |
| raise TemplateNotFound(template) |
| |
| with open(p, "rb") as f: |
| source = f.read() |
| |
| mtime = os.path.getmtime(p) |
| |
| def up_to_date() -> bool: |
| return os.path.isfile(p) and os.path.getmtime(p) == mtime |
| |
| else: |
| # Package is a zip file. |
| try: |
| source = self._loader.get_data(p) # type: ignore |
| except OSError as e: |
| raise TemplateNotFound(template) from e |
| |
| # Could use the zip's mtime for all template mtimes, but |
| # would need to safely reload the module if it's out of |
| # date, so just report it as always current. |
| up_to_date = None |
| |
| return source.decode(self.encoding), p, up_to_date |
| |
| def list_templates(self) -> t.List[str]: |
| results: t.List[str] = [] |
| |
| if self._archive is None: |
| # Package is a directory. |
| offset = len(self._template_root) |
| |
| for dirpath, _, filenames in os.walk(self._template_root): |
| dirpath = dirpath[offset:].lstrip(os.path.sep) |
| results.extend( |
| os.path.join(dirpath, name).replace(os.path.sep, "/") |
| for name in filenames |
| ) |
| else: |
| if not hasattr(self._loader, "_files"): |
| raise TypeError( |
| "This zip import does not have the required" |
| " metadata to list templates." |
| ) |
| |
| # Package is a zip file. |
| prefix = ( |
| self._template_root[len(self._archive) :].lstrip(os.path.sep) |
| + os.path.sep |
| ) |
| offset = len(prefix) |
| |
| for name in self._loader._files.keys(): # type: ignore |
| # Find names under the templates directory that aren't directories. |
| if name.startswith(prefix) and name[-1] != os.path.sep: |
| results.append(name[offset:].replace(os.path.sep, "/")) |
| |
| results.sort() |
| return results |
| |
| |
| class DictLoader(BaseLoader): |
| """Loads a template from a Python dict mapping template names to |
| template source. This loader is useful for unittesting: |
| |
| >>> loader = DictLoader({'index.html': 'source here'}) |
| |
| Because auto reloading is rarely useful this is disabled per default. |
| """ |
| |
| def __init__(self, mapping: t.Mapping[str, str]) -> None: |
| self.mapping = mapping |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, None, t.Callable[[], bool]]: |
| if template in self.mapping: |
| source = self.mapping[template] |
| return source, None, lambda: source == self.mapping.get(template) |
| raise TemplateNotFound(template) |
| |
| def list_templates(self) -> t.List[str]: |
| return sorted(self.mapping) |
| |
| |
| class FunctionLoader(BaseLoader): |
| """A loader that is passed a function which does the loading. The |
| function receives the name of the template and has to return either |
| a string with the template source, a tuple in the form ``(source, |
| filename, uptodatefunc)`` or `None` if the template does not exist. |
| |
| >>> def load_template(name): |
| ... if name == 'index.html': |
| ... return '...' |
| ... |
| >>> loader = FunctionLoader(load_template) |
| |
| The `uptodatefunc` is a function that is called if autoreload is enabled |
| and has to return `True` if the template is still up to date. For more |
| details have a look at :meth:`BaseLoader.get_source` which has the same |
| return value. |
| """ |
| |
| def __init__( |
| self, |
| load_func: t.Callable[ |
| [str], |
| t.Optional[ |
| t.Union[ |
| str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]] |
| ] |
| ], |
| ], |
| ) -> None: |
| self.load_func = load_func |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: |
| rv = self.load_func(template) |
| |
| if rv is None: |
| raise TemplateNotFound(template) |
| |
| if isinstance(rv, str): |
| return rv, None, None |
| |
| return rv |
| |
| |
| class PrefixLoader(BaseLoader): |
| """A loader that is passed a dict of loaders where each loader is bound |
| to a prefix. The prefix is delimited from the template by a slash per |
| default, which can be changed by setting the `delimiter` argument to |
| something else:: |
| |
| loader = PrefixLoader({ |
| 'app1': PackageLoader('mypackage.app1'), |
| 'app2': PackageLoader('mypackage.app2') |
| }) |
| |
| By loading ``'app1/index.html'`` the file from the app1 package is loaded, |
| by loading ``'app2/index.html'`` the file from the second. |
| """ |
| |
| def __init__( |
| self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/" |
| ) -> None: |
| self.mapping = mapping |
| self.delimiter = delimiter |
| |
| def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]: |
| try: |
| prefix, name = template.split(self.delimiter, 1) |
| loader = self.mapping[prefix] |
| except (ValueError, KeyError) as e: |
| raise TemplateNotFound(template) from e |
| return loader, name |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: |
| loader, name = self.get_loader(template) |
| try: |
| return loader.get_source(environment, name) |
| except TemplateNotFound as e: |
| # re-raise the exception with the correct filename here. |
| # (the one that includes the prefix) |
| raise TemplateNotFound(template) from e |
| |
| @internalcode |
| def load( |
| self, |
| environment: "Environment", |
| name: str, |
| globals: t.Optional[t.MutableMapping[str, t.Any]] = None, |
| ) -> "Template": |
| loader, local_name = self.get_loader(name) |
| try: |
| return loader.load(environment, local_name, globals) |
| except TemplateNotFound as e: |
| # re-raise the exception with the correct filename here. |
| # (the one that includes the prefix) |
| raise TemplateNotFound(name) from e |
| |
| def list_templates(self) -> t.List[str]: |
| result = [] |
| for prefix, loader in self.mapping.items(): |
| for template in loader.list_templates(): |
| result.append(prefix + self.delimiter + template) |
| return result |
| |
| |
| class ChoiceLoader(BaseLoader): |
| """This loader works like the `PrefixLoader` just that no prefix is |
| specified. If a template could not be found by one loader the next one |
| is tried. |
| |
| >>> loader = ChoiceLoader([ |
| ... FileSystemLoader('/path/to/user/templates'), |
| ... FileSystemLoader('/path/to/system/templates') |
| ... ]) |
| |
| This is useful if you want to allow users to override builtin templates |
| from a different location. |
| """ |
| |
| def __init__(self, loaders: t.Sequence[BaseLoader]) -> None: |
| self.loaders = loaders |
| |
| def get_source( |
| self, environment: "Environment", template: str |
| ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: |
| for loader in self.loaders: |
| try: |
| return loader.get_source(environment, template) |
| except TemplateNotFound: |
| pass |
| raise TemplateNotFound(template) |
| |
| @internalcode |
| def load( |
| self, |
| environment: "Environment", |
| name: str, |
| globals: t.Optional[t.MutableMapping[str, t.Any]] = None, |
| ) -> "Template": |
| for loader in self.loaders: |
| try: |
| return loader.load(environment, name, globals) |
| except TemplateNotFound: |
| pass |
| raise TemplateNotFound(name) |
| |
| def list_templates(self) -> t.List[str]: |
| found = set() |
| for loader in self.loaders: |
| found.update(loader.list_templates()) |
| return sorted(found) |
| |
| |
| class _TemplateModule(ModuleType): |
| """Like a normal module but with support for weak references""" |
| |
| |
| class ModuleLoader(BaseLoader): |
| """This loader loads templates from precompiled templates. |
| |
| Example usage: |
| |
| >>> loader = ChoiceLoader([ |
| ... ModuleLoader('/path/to/compiled/templates'), |
| ... FileSystemLoader('/path/to/templates') |
| ... ]) |
| |
| Templates can be precompiled with :meth:`Environment.compile_templates`. |
| """ |
| |
| has_source_access = False |
| |
| def __init__( |
| self, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]] |
| ) -> None: |
| package_name = f"_jinja2_module_templates_{id(self):x}" |
| |
| # create a fake module that looks for the templates in the |
| # path given. |
| mod = _TemplateModule(package_name) |
| |
| if not isinstance(path, abc.Iterable) or isinstance(path, str): |
| path = [path] |
| |
| mod.__path__ = [os.fspath(p) for p in path] |
| |
| sys.modules[package_name] = weakref.proxy( |
| mod, lambda x: sys.modules.pop(package_name, None) |
| ) |
| |
| # the only strong reference, the sys.modules entry is weak |
| # so that the garbage collector can remove it once the |
| # loader that created it goes out of business. |
| self.module = mod |
| self.package_name = package_name |
| |
| @staticmethod |
| def get_template_key(name: str) -> str: |
| return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() |
| |
| @staticmethod |
| def get_module_filename(name: str) -> str: |
| return ModuleLoader.get_template_key(name) + ".py" |
| |
| @internalcode |
| def load( |
| self, |
| environment: "Environment", |
| name: str, |
| globals: t.Optional[t.MutableMapping[str, t.Any]] = None, |
| ) -> "Template": |
| key = self.get_template_key(name) |
| module = f"{self.package_name}.{key}" |
| mod = getattr(self.module, module, None) |
| |
| if mod is None: |
| try: |
| mod = __import__(module, None, None, ["root"]) |
| except ImportError as e: |
| raise TemplateNotFound(name) from e |
| |
| # remove the entry from sys.modules, we only want the attribute |
| # on the module object we have stored on the loader. |
| sys.modules.pop(module, None) |
| |
| if globals is None: |
| globals = {} |
| |
| return environment.template_class.from_module_dict( |
| environment, mod.__dict__, globals |
| ) |