Spaces:
Running
Running
"""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") | |
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 | |
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) | |
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 | |
def get_template_key(name: str) -> str: | |
return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() | |
def get_module_filename(name: str) -> str: | |
return ModuleLoader.get_template_key(name) + ".py" | |
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 | |
) | |