123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- """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
- 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[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
- ],
- 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)
- if os.path.isfile(filename):
- break
- else:
- raise TemplateNotFound(template)
- with open(filename, encoding=self.encoding) as f:
- contents = f.read()
- 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
- 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():
- # 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[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
- ],
- ) -> 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
- )
|