17.12
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template import Origin, TemplateDoesNotExist
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
from .base import BaseEngine
|
||||
from .utils import csrf_input_lazy, csrf_token_lazy
|
||||
|
||||
|
||||
class TemplateStrings(BaseEngine):
|
||||
|
||||
app_dirname = 'template_strings'
|
||||
|
||||
def __init__(self, params):
|
||||
params = params.copy()
|
||||
options = params.pop('OPTIONS').copy()
|
||||
if options:
|
||||
raise ImproperlyConfigured(
|
||||
"Unknown options: {}".format(", ".join(options)))
|
||||
super().__init__(params)
|
||||
|
||||
def from_string(self, template_code):
|
||||
return Template(template_code)
|
||||
|
||||
def get_template(self, template_name):
|
||||
tried = []
|
||||
for template_file in self.iter_template_filenames(template_name):
|
||||
try:
|
||||
with open(template_file, encoding=settings.FILE_CHARSET) as fp:
|
||||
template_code = fp.read()
|
||||
except FileNotFoundError:
|
||||
tried.append((
|
||||
Origin(template_file, template_name, self),
|
||||
'Source does not exist',
|
||||
))
|
||||
else:
|
||||
return Template(template_code)
|
||||
raise TemplateDoesNotExist(template_name, tried=tried, backend=self)
|
||||
|
||||
|
||||
class Template(string.Template):
|
||||
|
||||
def render(self, context=None, request=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
else:
|
||||
context = {k: conditional_escape(v) for k, v in context.items()}
|
||||
if request is not None:
|
||||
context['csrf_input'] = csrf_input_lazy(request)
|
||||
context['csrf_token'] = csrf_token_lazy(request)
|
||||
return self.safe_substitute(context)
|
||||
@@ -0,0 +1,108 @@
|
||||
import jinja2
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import TemplateDoesNotExist, TemplateSyntaxError
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class Jinja2(BaseEngine):
|
||||
|
||||
app_dirname = 'jinja2'
|
||||
|
||||
def __init__(self, params):
|
||||
params = params.copy()
|
||||
options = params.pop('OPTIONS').copy()
|
||||
super().__init__(params)
|
||||
|
||||
self.context_processors = options.pop('context_processors', [])
|
||||
|
||||
environment = options.pop('environment', 'jinja2.Environment')
|
||||
environment_cls = import_string(environment)
|
||||
|
||||
if 'loader' not in options:
|
||||
options['loader'] = jinja2.FileSystemLoader(self.template_dirs)
|
||||
options.setdefault('autoescape', True)
|
||||
options.setdefault('auto_reload', settings.DEBUG)
|
||||
options.setdefault('undefined',
|
||||
jinja2.DebugUndefined if settings.DEBUG else jinja2.Undefined)
|
||||
|
||||
self.env = environment_cls(**options)
|
||||
|
||||
def from_string(self, template_code):
|
||||
return Template(self.env.from_string(template_code), self)
|
||||
|
||||
def get_template(self, template_name):
|
||||
try:
|
||||
return Template(self.env.get_template(template_name), self)
|
||||
except jinja2.TemplateNotFound as exc:
|
||||
raise TemplateDoesNotExist(exc.name, backend=self) from exc
|
||||
except jinja2.TemplateSyntaxError as exc:
|
||||
new = TemplateSyntaxError(exc.args)
|
||||
new.template_debug = get_exception_info(exc)
|
||||
raise new from exc
|
||||
|
||||
@cached_property
|
||||
def template_context_processors(self):
|
||||
return [import_string(path) for path in self.context_processors]
|
||||
|
||||
|
||||
class Template:
|
||||
|
||||
def __init__(self, template, backend):
|
||||
self.template = template
|
||||
self.backend = backend
|
||||
self.origin = Origin(
|
||||
name=template.filename, template_name=template.name,
|
||||
)
|
||||
|
||||
def render(self, context=None, request=None):
|
||||
from .utils import csrf_input_lazy, csrf_token_lazy
|
||||
if context is None:
|
||||
context = {}
|
||||
if request is not None:
|
||||
context['request'] = request
|
||||
context['csrf_input'] = csrf_input_lazy(request)
|
||||
context['csrf_token'] = csrf_token_lazy(request)
|
||||
for context_processor in self.backend.template_context_processors:
|
||||
context.update(context_processor(request))
|
||||
return self.template.render(context)
|
||||
|
||||
|
||||
class Origin:
|
||||
"""
|
||||
A container to hold debug information as described in the template API
|
||||
documentation.
|
||||
"""
|
||||
def __init__(self, name, template_name):
|
||||
self.name = name
|
||||
self.template_name = template_name
|
||||
|
||||
|
||||
def get_exception_info(exception):
|
||||
"""
|
||||
Format exception information for display on the debug page using the
|
||||
structure described in the template API documentation.
|
||||
"""
|
||||
context_lines = 10
|
||||
lineno = exception.lineno
|
||||
lines = list(enumerate(exception.source.strip().split("\n"), start=1))
|
||||
during = lines[lineno - 1][1]
|
||||
total = len(lines)
|
||||
top = max(0, lineno - context_lines - 1)
|
||||
bottom = min(total, lineno + context_lines)
|
||||
|
||||
return {
|
||||
'name': exception.filename,
|
||||
'message': exception.message,
|
||||
'source_lines': lines[top:bottom],
|
||||
'line': lineno,
|
||||
'before': '',
|
||||
'during': during,
|
||||
'after': '',
|
||||
'total': total,
|
||||
'top': top,
|
||||
'bottom': bottom,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.middleware.csrf import get_token
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import SafeText
|
||||
|
||||
|
||||
def csrf_input(request):
|
||||
return format_html(
|
||||
'<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
|
||||
get_token(request))
|
||||
|
||||
|
||||
csrf_input_lazy = lazy(csrf_input, SafeText, str)
|
||||
csrf_token_lazy = lazy(get_token, str)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
|
||||
# Hard-coded processor for easier use of CSRF protection.
|
||||
_builtin_context_processors = ('django.template.context_processors.csrf',)
|
||||
|
||||
|
||||
class ContextPopException(Exception):
|
||||
"pop() has been called more times than push()"
|
||||
pass
|
||||
|
||||
|
||||
class ContextDict(dict):
|
||||
def __init__(self, context, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
context.dicts.append(self)
|
||||
self.context = context
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.context.pop()
|
||||
|
||||
|
||||
class BaseContext:
|
||||
def __init__(self, dict_=None):
|
||||
self._reset_dicts(dict_)
|
||||
|
||||
def _reset_dicts(self, value=None):
|
||||
builtins = {'True': True, 'False': False, 'None': None}
|
||||
self.dicts = [builtins]
|
||||
if value is not None:
|
||||
self.dicts.append(value)
|
||||
|
||||
def __copy__(self):
|
||||
duplicate = copy(super())
|
||||
duplicate.dicts = self.dicts[:]
|
||||
return duplicate
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.dicts)
|
||||
|
||||
def __iter__(self):
|
||||
return reversed(self.dicts)
|
||||
|
||||
def push(self, *args, **kwargs):
|
||||
dicts = []
|
||||
for d in args:
|
||||
if isinstance(d, BaseContext):
|
||||
dicts += d.dicts[1:]
|
||||
else:
|
||||
dicts.append(d)
|
||||
return ContextDict(self, *dicts, **kwargs)
|
||||
|
||||
def pop(self):
|
||||
if len(self.dicts) == 1:
|
||||
raise ContextPopException
|
||||
return self.dicts.pop()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"Set a variable in the current context"
|
||||
self.dicts[-1][key] = value
|
||||
|
||||
def set_upward(self, key, value):
|
||||
"""
|
||||
Set a variable in one of the higher contexts if it exists there,
|
||||
otherwise in the current context.
|
||||
"""
|
||||
context = self.dicts[-1]
|
||||
for d in reversed(self.dicts):
|
||||
if key in d:
|
||||
context = d
|
||||
break
|
||||
context[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
"Get a variable's value, starting at the current context and going upward"
|
||||
for d in reversed(self.dicts):
|
||||
if key in d:
|
||||
return d[key]
|
||||
raise KeyError(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
"Delete a variable from the current context"
|
||||
del self.dicts[-1][key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return any(key in d for d in self.dicts)
|
||||
|
||||
def get(self, key, otherwise=None):
|
||||
for d in reversed(self.dicts):
|
||||
if key in d:
|
||||
return d[key]
|
||||
return otherwise
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def new(self, values=None):
|
||||
"""
|
||||
Return a new context with the same properties, but with only the
|
||||
values given in 'values' stored.
|
||||
"""
|
||||
new_context = copy(self)
|
||||
new_context._reset_dicts(values)
|
||||
return new_context
|
||||
|
||||
def flatten(self):
|
||||
"""
|
||||
Return self.dicts as one dictionary.
|
||||
"""
|
||||
flat = {}
|
||||
for d in self.dicts:
|
||||
flat.update(d)
|
||||
return flat
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Compare two contexts by comparing theirs 'dicts' attributes.
|
||||
"""
|
||||
return (
|
||||
isinstance(other, BaseContext) and
|
||||
# because dictionaries can be put in different order
|
||||
# we have to flatten them like in templates
|
||||
self.flatten() == other.flatten()
|
||||
)
|
||||
|
||||
|
||||
class Context(BaseContext):
|
||||
"A stack container for variable context"
|
||||
def __init__(self, dict_=None, autoescape=True, use_l10n=None, use_tz=None):
|
||||
self.autoescape = autoescape
|
||||
self.use_l10n = use_l10n
|
||||
self.use_tz = use_tz
|
||||
self.template_name = "unknown"
|
||||
self.render_context = RenderContext()
|
||||
# Set to the original template -- as opposed to extended or included
|
||||
# templates -- during rendering, see bind_template.
|
||||
self.template = None
|
||||
super().__init__(dict_)
|
||||
|
||||
@contextmanager
|
||||
def bind_template(self, template):
|
||||
if self.template is not None:
|
||||
raise RuntimeError("Context is already bound to a template")
|
||||
self.template = template
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.template = None
|
||||
|
||||
def __copy__(self):
|
||||
duplicate = super().__copy__()
|
||||
duplicate.render_context = copy(self.render_context)
|
||||
return duplicate
|
||||
|
||||
def update(self, other_dict):
|
||||
"Push other_dict to the stack of dictionaries in the Context"
|
||||
if not hasattr(other_dict, '__getitem__'):
|
||||
raise TypeError('other_dict must be a mapping (dictionary-like) object.')
|
||||
if isinstance(other_dict, BaseContext):
|
||||
other_dict = other_dict.dicts[1:].pop()
|
||||
return ContextDict(self, other_dict)
|
||||
|
||||
|
||||
class RenderContext(BaseContext):
|
||||
"""
|
||||
A stack container for storing Template state.
|
||||
|
||||
RenderContext simplifies the implementation of template Nodes by providing a
|
||||
safe place to store state between invocations of a node's `render` method.
|
||||
|
||||
The RenderContext also provides scoping rules that are more sensible for
|
||||
'template local' variables. The render context stack is pushed before each
|
||||
template is rendered, creating a fresh scope with nothing in it. Name
|
||||
resolution fails if a variable is not found at the top of the RequestContext
|
||||
stack. Thus, variables are local to a specific template and don't affect the
|
||||
rendering of other templates as they would if they were stored in the normal
|
||||
template context.
|
||||
"""
|
||||
template = None
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.dicts[-1]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.dicts[-1]
|
||||
|
||||
def get(self, key, otherwise=None):
|
||||
return self.dicts[-1].get(key, otherwise)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.dicts[-1][key]
|
||||
|
||||
@contextmanager
|
||||
def push_state(self, template, isolated_context=True):
|
||||
initial = self.template
|
||||
self.template = template
|
||||
if isolated_context:
|
||||
self.push()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.template = initial
|
||||
if isolated_context:
|
||||
self.pop()
|
||||
|
||||
|
||||
class RequestContext(Context):
|
||||
"""
|
||||
This subclass of template.Context automatically populates itself using
|
||||
the processors defined in the engine's configuration.
|
||||
Additional processors can be specified as a list of callables
|
||||
using the "processors" keyword argument.
|
||||
"""
|
||||
def __init__(self, request, dict_=None, processors=None, use_l10n=None, use_tz=None, autoescape=True):
|
||||
super().__init__(dict_, use_l10n=use_l10n, use_tz=use_tz, autoescape=autoescape)
|
||||
self.request = request
|
||||
self._processors = () if processors is None else tuple(processors)
|
||||
self._processors_index = len(self.dicts)
|
||||
|
||||
# placeholder for context processors output
|
||||
self.update({})
|
||||
|
||||
# empty dict for any new modifications
|
||||
# (so that context processors don't overwrite them)
|
||||
self.update({})
|
||||
|
||||
@contextmanager
|
||||
def bind_template(self, template):
|
||||
if self.template is not None:
|
||||
raise RuntimeError("Context is already bound to a template")
|
||||
|
||||
self.template = template
|
||||
# Set context processors according to the template engine's settings.
|
||||
processors = (template.engine.template_context_processors +
|
||||
self._processors)
|
||||
updates = {}
|
||||
for processor in processors:
|
||||
updates.update(processor(self.request))
|
||||
self.dicts[self._processors_index] = updates
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.template = None
|
||||
# Unset context processors.
|
||||
self.dicts[self._processors_index] = {}
|
||||
|
||||
def new(self, values=None):
|
||||
new_context = super().new(values)
|
||||
# This is for backwards-compatibility: RequestContexts created via
|
||||
# Context.new don't include values from context processors.
|
||||
if hasattr(new_context, '_processors_index'):
|
||||
del new_context._processors_index
|
||||
return new_context
|
||||
|
||||
|
||||
def make_context(context, request=None, **kwargs):
|
||||
"""
|
||||
Create a suitable Context from a plain dict and optionally an HttpRequest.
|
||||
"""
|
||||
if context is not None and not isinstance(context, dict):
|
||||
raise TypeError('context must be a dict rather than %s.' % context.__class__.__name__)
|
||||
if request is None:
|
||||
context = Context(context, **kwargs)
|
||||
else:
|
||||
# The following pattern is required to ensure values from
|
||||
# context override those from template context processors.
|
||||
original_context = context
|
||||
context = RequestContext(request, **kwargs)
|
||||
if original_context:
|
||||
context.push(original_context)
|
||||
return context
|
||||
@@ -0,0 +1,49 @@
|
||||
from django.template import Template, TemplateDoesNotExist
|
||||
|
||||
|
||||
class Loader:
|
||||
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
|
||||
def get_template(self, template_name, skip=None):
|
||||
"""
|
||||
Call self.get_template_sources() and return a Template object for
|
||||
the first template matching template_name. If skip is provided, ignore
|
||||
template origins in skip. This is used to avoid recursion during
|
||||
template extending.
|
||||
"""
|
||||
tried = []
|
||||
|
||||
for origin in self.get_template_sources(template_name):
|
||||
if skip is not None and origin in skip:
|
||||
tried.append((origin, 'Skipped'))
|
||||
continue
|
||||
|
||||
try:
|
||||
contents = self.get_contents(origin)
|
||||
except TemplateDoesNotExist:
|
||||
tried.append((origin, 'Source does not exist'))
|
||||
continue
|
||||
else:
|
||||
return Template(
|
||||
contents, origin, origin.template_name, self.engine,
|
||||
)
|
||||
|
||||
raise TemplateDoesNotExist(template_name, tried=tried)
|
||||
|
||||
def get_template_sources(self, template_name):
|
||||
"""
|
||||
An iterator that yields possible matching template paths for a
|
||||
template name.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'subclasses of Loader must provide a get_template_sources() method'
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset any state maintained by the loader instance (e.g. cached
|
||||
templates or cached loader modules).
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Wrapper class that takes a list of template loaders as an argument and attempts
|
||||
to load templates from them in order, caching the result.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.template import TemplateDoesNotExist
|
||||
from django.template.backends.django import copy_exception
|
||||
|
||||
from .base import Loader as BaseLoader
|
||||
|
||||
|
||||
class Loader(BaseLoader):
|
||||
|
||||
def __init__(self, engine, loaders):
|
||||
self.template_cache = {}
|
||||
self.get_template_cache = {}
|
||||
self.loaders = engine.get_template_loaders(loaders)
|
||||
super().__init__(engine)
|
||||
|
||||
def get_contents(self, origin):
|
||||
return origin.loader.get_contents(origin)
|
||||
|
||||
def get_template(self, template_name, skip=None):
|
||||
"""
|
||||
Perform the caching that gives this loader its name. Often many of the
|
||||
templates attempted will be missing, so memory use is of concern here.
|
||||
To keep it in check, caching behavior is a little complicated when a
|
||||
template is not found. See ticket #26306 for more details.
|
||||
|
||||
With template debugging disabled, cache the TemplateDoesNotExist class
|
||||
for every missing template and raise a new instance of it after
|
||||
fetching it from the cache.
|
||||
|
||||
With template debugging enabled, a unique TemplateDoesNotExist object
|
||||
is cached for each missing template to preserve debug data. When
|
||||
raising an exception, Python sets __traceback__, __context__, and
|
||||
__cause__ attributes on it. Those attributes can contain references to
|
||||
all sorts of objects up the call chain and caching them creates a
|
||||
memory leak. Thus, unraised copies of the exceptions are cached and
|
||||
copies of those copies are raised after they're fetched from the cache.
|
||||
"""
|
||||
key = self.cache_key(template_name, skip)
|
||||
cached = self.get_template_cache.get(key)
|
||||
if cached:
|
||||
if isinstance(cached, type) and issubclass(cached, TemplateDoesNotExist):
|
||||
raise cached(template_name)
|
||||
elif isinstance(cached, TemplateDoesNotExist):
|
||||
raise copy_exception(cached)
|
||||
return cached
|
||||
|
||||
try:
|
||||
template = super().get_template(template_name, skip)
|
||||
except TemplateDoesNotExist as e:
|
||||
self.get_template_cache[key] = copy_exception(e) if self.engine.debug else TemplateDoesNotExist
|
||||
raise
|
||||
else:
|
||||
self.get_template_cache[key] = template
|
||||
|
||||
return template
|
||||
|
||||
def get_template_sources(self, template_name):
|
||||
for loader in self.loaders:
|
||||
yield from loader.get_template_sources(template_name)
|
||||
|
||||
def cache_key(self, template_name, skip=None):
|
||||
"""
|
||||
Generate a cache key for the template name, dirs, and skip.
|
||||
|
||||
If skip is provided, only origins that match template_name are included
|
||||
in the cache key. This ensures each template is only parsed and cached
|
||||
once if contained in different extend chains like:
|
||||
|
||||
x -> a -> a
|
||||
y -> a -> a
|
||||
z -> a -> a
|
||||
"""
|
||||
dirs_prefix = ''
|
||||
skip_prefix = ''
|
||||
|
||||
if skip:
|
||||
matching = [origin.name for origin in skip if origin.template_name == template_name]
|
||||
if matching:
|
||||
skip_prefix = self.generate_hash(matching)
|
||||
|
||||
return '-'.join(s for s in (str(template_name), skip_prefix, dirs_prefix) if s)
|
||||
|
||||
def generate_hash(self, values):
|
||||
return hashlib.sha1('|'.join(values).encode()).hexdigest()
|
||||
|
||||
def reset(self):
|
||||
"Empty the template cache."
|
||||
self.template_cache.clear()
|
||||
self.get_template_cache.clear()
|
||||
@@ -0,0 +1,107 @@
|
||||
import functools
|
||||
from collections import Counter, OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
class InvalidTemplateEngineError(ImproperlyConfigured):
|
||||
pass
|
||||
|
||||
|
||||
class EngineHandler:
|
||||
def __init__(self, templates=None):
|
||||
"""
|
||||
templates is an optional list of template engine definitions
|
||||
(structured like settings.TEMPLATES).
|
||||
"""
|
||||
self._templates = templates
|
||||
self._engines = {}
|
||||
|
||||
@cached_property
|
||||
def templates(self):
|
||||
if self._templates is None:
|
||||
self._templates = settings.TEMPLATES
|
||||
|
||||
templates = OrderedDict()
|
||||
backend_names = []
|
||||
for tpl in self._templates:
|
||||
try:
|
||||
# This will raise an exception if 'BACKEND' doesn't exist or
|
||||
# isn't a string containing at least one dot.
|
||||
default_name = tpl['BACKEND'].rsplit('.', 2)[-2]
|
||||
except Exception:
|
||||
invalid_backend = tpl.get('BACKEND', '<not defined>')
|
||||
raise ImproperlyConfigured(
|
||||
"Invalid BACKEND for a template engine: {}. Check "
|
||||
"your TEMPLATES setting.".format(invalid_backend))
|
||||
|
||||
tpl = {
|
||||
'NAME': default_name,
|
||||
'DIRS': [],
|
||||
'APP_DIRS': False,
|
||||
'OPTIONS': {},
|
||||
**tpl,
|
||||
}
|
||||
|
||||
templates[tpl['NAME']] = tpl
|
||||
backend_names.append(tpl['NAME'])
|
||||
|
||||
counts = Counter(backend_names)
|
||||
duplicates = [alias for alias, count in counts.most_common() if count > 1]
|
||||
if duplicates:
|
||||
raise ImproperlyConfigured(
|
||||
"Template engine aliases aren't unique, duplicates: {}. "
|
||||
"Set a unique NAME for each engine in settings.TEMPLATES."
|
||||
.format(", ".join(duplicates)))
|
||||
|
||||
return templates
|
||||
|
||||
def __getitem__(self, alias):
|
||||
try:
|
||||
return self._engines[alias]
|
||||
except KeyError:
|
||||
try:
|
||||
params = self.templates[alias]
|
||||
except KeyError:
|
||||
raise InvalidTemplateEngineError(
|
||||
"Could not find config for '{}' "
|
||||
"in settings.TEMPLATES".format(alias))
|
||||
|
||||
# If importing or initializing the backend raises an exception,
|
||||
# self._engines[alias] isn't set and this code may get executed
|
||||
# again, so we must preserve the original params. See #24265.
|
||||
params = params.copy()
|
||||
backend = params.pop('BACKEND')
|
||||
engine_cls = import_string(backend)
|
||||
engine = engine_cls(params)
|
||||
|
||||
self._engines[alias] = engine
|
||||
return engine
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.templates)
|
||||
|
||||
def all(self):
|
||||
return [self[alias] for alias in self]
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_app_template_dirs(dirname):
|
||||
"""
|
||||
Return an iterable of paths of directories to load app templates from.
|
||||
|
||||
dirname is the name of the subdirectory containing templates inside
|
||||
installed applications.
|
||||
"""
|
||||
template_dirs = [
|
||||
str(Path(app_config.path) / dirname)
|
||||
for app_config in apps.get_app_configs()
|
||||
if app_config.path and (Path(app_config.path) / dirname).is_dir()
|
||||
]
|
||||
# Immutable return value because it will be cached and shared by callers.
|
||||
return tuple(template_dirs)
|
||||
Reference in New Issue
Block a user