Varia's website
https://varia.zone
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
505 lines
17 KiB
505 lines
17 KiB
2 weeks ago
|
# -*- coding: utf-8 -*-
|
||
|
"""API and implementations for loading templates from different data
|
||
|
sources.
|
||
|
"""
|
||
|
import os
|
||
|
import sys
|
||
|
import weakref
|
||
|
from hashlib import sha1
|
||
|
from os import path
|
||
|
from types import ModuleType
|
||
|
|
||
|
from ._compat import abc
|
||
|
from ._compat import fspath
|
||
|
from ._compat import iteritems
|
||
|
from ._compat import string_types
|
||
|
from .exceptions import TemplateNotFound
|
||
|
from .utils import internalcode
|
||
|
from .utils import open_if_exists
|
||
|
|
||
|
|
||
|
def split_template_path(template):
|
||
|
"""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 (
|
||
|
path.sep in piece
|
||
|
or (path.altsep and path.altsep in piece)
|
||
|
or piece == path.pardir
|
||
|
):
|
||
|
raise TemplateNotFound(template)
|
||
|
elif piece and piece != ".":
|
||
|
pieces.append(piece)
|
||
|
return pieces
|
||
|
|
||
|
|
||
|
class BaseLoader(object):
|
||
|
"""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 file(path) as f:
|
||
|
source = f.read().decode('utf-8')
|
||
|
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, template):
|
||
|
"""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 unicode string or a ASCII bytestring. 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(
|
||
|
"%s cannot provide access to the source" % self.__class__.__name__
|
||
|
)
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
def list_templates(self):
|
||
|
"""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, name, globals=None):
|
||
|
"""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):
|
||
|
"""Loads templates from the file system. This loader can find templates
|
||
|
in folders on the file system and is the preferred way to load them.
|
||
|
|
||
|
The loader takes the path to the templates as string, or if multiple
|
||
|
locations are wanted a list of them which is then looked up in the
|
||
|
given order::
|
||
|
|
||
|
>>> loader = FileSystemLoader('/path/to/templates')
|
||
|
>>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
|
||
|
|
||
|
Per default the template encoding is ``'utf-8'`` which can be changed
|
||
|
by setting the `encoding` parameter to something else.
|
||
|
|
||
|
To follow symbolic links, set the *followlinks* parameter to ``True``::
|
||
|
|
||
|
>>> loader = FileSystemLoader('/path/to/templates', followlinks=True)
|
||
|
|
||
|
.. versionchanged:: 2.8
|
||
|
The ``followlinks`` parameter was added.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, searchpath, encoding="utf-8", followlinks=False):
|
||
|
if not isinstance(searchpath, abc.Iterable) or isinstance(
|
||
|
searchpath, string_types
|
||
|
):
|
||
|
searchpath = [searchpath]
|
||
|
|
||
|
# In Python 3.5, os.path.join doesn't support Path. This can be
|
||
|
# simplified to list(searchpath) when Python 3.5 is dropped.
|
||
|
self.searchpath = [fspath(p) for p in searchpath]
|
||
|
|
||
|
self.encoding = encoding
|
||
|
self.followlinks = followlinks
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
pieces = split_template_path(template)
|
||
|
for searchpath in self.searchpath:
|
||
|
filename = path.join(searchpath, *pieces)
|
||
|
f = open_if_exists(filename)
|
||
|
if f is None:
|
||
|
continue
|
||
|
try:
|
||
|
contents = f.read().decode(self.encoding)
|
||
|
finally:
|
||
|
f.close()
|
||
|
|
||
|
mtime = path.getmtime(filename)
|
||
|
|
||
|
def uptodate():
|
||
|
try:
|
||
|
return path.getmtime(filename) == mtime
|
||
|
except OSError:
|
||
|
return False
|
||
|
|
||
|
return contents, filename, uptodate
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
def list_templates(self):
|
||
|
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 python eggs or packages. It is constructed with
|
||
|
the name of the python package and the path to the templates in that
|
||
|
package::
|
||
|
|
||
|
loader = PackageLoader('mypackage', 'views')
|
||
|
|
||
|
If the package path is not given, ``'templates'`` is assumed.
|
||
|
|
||
|
Per default the template encoding is ``'utf-8'`` which can be changed
|
||
|
by setting the `encoding` parameter to something else. Due to the nature
|
||
|
of eggs it's only possible to reload templates if the package was loaded
|
||
|
from the file system and not a zip file.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, package_name, package_path="templates", encoding="utf-8"):
|
||
|
from pkg_resources import DefaultProvider
|
||
|
from pkg_resources import get_provider
|
||
|
from pkg_resources import ResourceManager
|
||
|
|
||
|
provider = get_provider(package_name)
|
||
|
self.encoding = encoding
|
||
|
self.manager = ResourceManager()
|
||
|
self.filesystem_bound = isinstance(provider, DefaultProvider)
|
||
|
self.provider = provider
|
||
|
self.package_path = package_path
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
pieces = split_template_path(template)
|
||
|
p = "/".join((self.package_path,) + tuple(pieces))
|
||
|
|
||
|
if not self.provider.has_resource(p):
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
filename = uptodate = None
|
||
|
|
||
|
if self.filesystem_bound:
|
||
|
filename = self.provider.get_resource_filename(self.manager, p)
|
||
|
mtime = path.getmtime(filename)
|
||
|
|
||
|
def uptodate():
|
||
|
try:
|
||
|
return path.getmtime(filename) == mtime
|
||
|
except OSError:
|
||
|
return False
|
||
|
|
||
|
source = self.provider.get_resource_string(self.manager, p)
|
||
|
return source.decode(self.encoding), filename, uptodate
|
||
|
|
||
|
def list_templates(self):
|
||
|
path = self.package_path
|
||
|
|
||
|
if path[:2] == "./":
|
||
|
path = path[2:]
|
||
|
elif path == ".":
|
||
|
path = ""
|
||
|
|
||
|
offset = len(path)
|
||
|
results = []
|
||
|
|
||
|
def _walk(path):
|
||
|
for filename in self.provider.resource_listdir(path):
|
||
|
fullname = path + "/" + filename
|
||
|
|
||
|
if self.provider.resource_isdir(fullname):
|
||
|
_walk(fullname)
|
||
|
else:
|
||
|
results.append(fullname[offset:].lstrip("/"))
|
||
|
|
||
|
_walk(path)
|
||
|
results.sort()
|
||
|
return results
|
||
|
|
||
|
|
||
|
class DictLoader(BaseLoader):
|
||
|
"""Loads a template from a python dict. It's passed a dict of unicode
|
||
|
strings bound to template names. 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):
|
||
|
self.mapping = mapping
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
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):
|
||
|
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
|
||
|
an unicode 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):
|
||
|
self.load_func = load_func
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
rv = self.load_func(template)
|
||
|
if rv is None:
|
||
|
raise TemplateNotFound(template)
|
||
|
elif isinstance(rv, string_types):
|
||
|
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, delimiter="/"):
|
||
|
self.mapping = mapping
|
||
|
self.delimiter = delimiter
|
||
|
|
||
|
def get_loader(self, template):
|
||
|
try:
|
||
|
prefix, name = template.split(self.delimiter, 1)
|
||
|
loader = self.mapping[prefix]
|
||
|
except (ValueError, KeyError):
|
||
|
raise TemplateNotFound(template)
|
||
|
return loader, name
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
loader, name = self.get_loader(template)
|
||
|
try:
|
||
|
return loader.get_source(environment, name)
|
||
|
except TemplateNotFound:
|
||
|
# re-raise the exception with the correct filename here.
|
||
|
# (the one that includes the prefix)
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
@internalcode
|
||
|
def load(self, environment, name, globals=None):
|
||
|
loader, local_name = self.get_loader(name)
|
||
|
try:
|
||
|
return loader.load(environment, local_name, globals)
|
||
|
except TemplateNotFound:
|
||
|
# re-raise the exception with the correct filename here.
|
||
|
# (the one that includes the prefix)
|
||
|
raise TemplateNotFound(name)
|
||
|
|
||
|
def list_templates(self):
|
||
|
result = []
|
||
|
for prefix, loader in iteritems(self.mapping):
|
||
|
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):
|
||
|
self.loaders = loaders
|
||
|
|
||
|
def get_source(self, environment, template):
|
||
|
for loader in self.loaders:
|
||
|
try:
|
||
|
return loader.get_source(environment, template)
|
||
|
except TemplateNotFound:
|
||
|
pass
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
@internalcode
|
||
|
def load(self, environment, name, globals=None):
|
||
|
for loader in self.loaders:
|
||
|
try:
|
||
|
return loader.load(environment, name, globals)
|
||
|
except TemplateNotFound:
|
||
|
pass
|
||
|
raise TemplateNotFound(name)
|
||
|
|
||
|
def list_templates(self):
|
||
|
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):
|
||
|
package_name = "_jinja2_module_templates_%x" % id(self)
|
||
|
|
||
|
# 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, string_types):
|
||
|
path = [path]
|
||
|
|
||
|
mod.__path__ = [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):
|
||
|
return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
|
||
|
|
||
|
@staticmethod
|
||
|
def get_module_filename(name):
|
||
|
return ModuleLoader.get_template_key(name) + ".py"
|
||
|
|
||
|
@internalcode
|
||
|
def load(self, environment, name, globals=None):
|
||
|
key = self.get_template_key(name)
|
||
|
module = "%s.%s" % (self.package_name, key)
|
||
|
mod = getattr(self.module, module, None)
|
||
|
if mod is None:
|
||
|
try:
|
||
|
mod = __import__(module, None, None, ["root"])
|
||
|
except ImportError:
|
||
|
raise TemplateNotFound(name)
|
||
|
|
||
|
# 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)
|
||
|
|
||
|
return environment.template_class.from_module_dict(
|
||
|
environment, mod.__dict__, globals
|
||
|
)
|