|
"""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) |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
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 = {} |
|
|
|
|
|
|
|
source, filename, uptodate = self.get_source(environment, name) |
|
|
|
|
|
|
|
bcc = environment.bytecode_cache |
|
if bcc is not None: |
|
bucket = bcc.get_bucket(environment, name, filename, source) |
|
code = bucket.code |
|
|
|
|
|
|
|
if code is None: |
|
code = environment.compile(source, name, filename) |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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)) |
|
template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep) |
|
else: |
|
roots: t.List[str] = [] |
|
|
|
|
|
|
|
if spec.submodule_search_locations: |
|
roots.extend(spec.submodule_search_locations) |
|
|
|
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]]]: |
|
|
|
|
|
|
|
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: |
|
|
|
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: |
|
|
|
try: |
|
source = self._loader.get_data(p) |
|
except OSError as e: |
|
raise TemplateNotFound(template) from e |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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." |
|
) |
|
|
|
|
|
prefix = ( |
|
self._template_root[len(self._archive) :].lstrip(os.path.sep) |
|
+ os.path.sep |
|
) |
|
offset = len(prefix) |
|
|
|
for name in self._loader._files.keys(): |
|
|
|
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: |
|
|
|
|
|
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: |
|
|
|
|
|
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}" |
|
|
|
|
|
|
|
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) |
|
) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
sys.modules.pop(module, None) |
|
|
|
if globals is None: |
|
globals = {} |
|
|
|
return environment.template_class.from_module_dict( |
|
environment, mod.__dict__, globals |
|
) |
|
|