änderung1
This commit is contained in:
1 parent
cc1d096bc3
commit
cbdda613b7
2098 files changed
+274524
No files matched your search
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Django's support for templates.
|
||||
|
||||
The django.template namespace contains two independent subsystems:
|
||||
|
||||
1. Multiple Template Engines: support for pluggable template backends,
|
||||
built-in backends and backend-independent APIs
|
||||
2. Django Template Language: Django's own template engine, including its
|
||||
built-in loaders, context processors, tags and filters.
|
||||
|
||||
Ideally these subsystems would be implemented in distinct packages. However
|
||||
keeping them together made the implementation of Multiple Template Engines
|
||||
less disruptive .
|
||||
|
||||
Here's a breakdown of which modules belong to which subsystem.
|
||||
|
||||
Multiple Template Engines:
|
||||
|
||||
- django.template.backends.*
|
||||
- django.template.loader
|
||||
- django.template.response
|
||||
|
||||
Django Template Language:
|
||||
|
||||
- django.template.base
|
||||
- django.template.context
|
||||
- django.template.context_processors
|
||||
- django.template.loaders.*
|
||||
- django.template.debug
|
||||
- django.template.defaultfilters
|
||||
- django.template.defaulttags
|
||||
- django.template.engine
|
||||
- django.template.loader_tags
|
||||
- django.template.smartif
|
||||
|
||||
Shared:
|
||||
|
||||
- django.template.utils
|
||||
|
||||
"""
|
||||
|
||||
# Multiple Template Engines
|
||||
|
||||
from .engine import Engine
|
||||
from .utils import EngineHandler
|
||||
|
||||
engines = EngineHandler()
|
||||
|
||||
__all__ = ('Engine', 'engines')
|
||||
|
||||
|
||||
# Django Template Language
|
||||
|
||||
# Public exceptions
|
||||
from .base import VariableDoesNotExist # NOQA isort:skip
|
||||
from .context import ContextPopException # NOQA isort:skip
|
||||
from .exceptions import TemplateDoesNotExist, TemplateSyntaxError # NOQA isort:skip
|
||||
|
||||
# Template parts
|
||||
from .base import ( # NOQA isort:skip
|
||||
Context, Node, NodeList, Origin, RequestContext, Template, Variable,
|
||||
)
|
||||
|
||||
# Library management
|
||||
from .library import Library # NOQA isort:skip
|
||||
|
||||
|
||||
__all__ += ('Template', 'Context', 'RequestContext')
|
||||
@@ -0,0 +1,81 @@
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured, SuspiciousFileOperation,
|
||||
)
|
||||
from django.template.utils import get_app_template_dirs
|
||||
from django.utils._os import safe_join
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class BaseEngine:
|
||||
|
||||
# Core methods: engines have to provide their own implementation
|
||||
# (except for from_string which is optional).
|
||||
|
||||
def __init__(self, params):
|
||||
"""
|
||||
Initialize the template engine.
|
||||
|
||||
`params` is a dict of configuration settings.
|
||||
"""
|
||||
params = params.copy()
|
||||
self.name = params.pop('NAME')
|
||||
self.dirs = list(params.pop('DIRS'))
|
||||
self.app_dirs = params.pop('APP_DIRS')
|
||||
if params:
|
||||
raise ImproperlyConfigured(
|
||||
"Unknown parameters: {}".format(", ".join(params)))
|
||||
|
||||
@property
|
||||
def app_dirname(self):
|
||||
raise ImproperlyConfigured(
|
||||
"{} doesn't support loading templates from installed "
|
||||
"applications.".format(self.__class__.__name__))
|
||||
|
||||
def from_string(self, template_code):
|
||||
"""
|
||||
Create and return a template for the given source code.
|
||||
|
||||
This method is optional.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseEngine should provide "
|
||||
"a from_string() method")
|
||||
|
||||
def get_template(self, template_name):
|
||||
"""
|
||||
Load and return a template for the given name.
|
||||
|
||||
Raise TemplateDoesNotExist if no such template exists.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseEngine must provide "
|
||||
"a get_template() method")
|
||||
|
||||
# Utility methods: they are provided to minimize code duplication and
|
||||
# security issues in third-party backends.
|
||||
|
||||
@cached_property
|
||||
def template_dirs(self):
|
||||
"""
|
||||
Return a list of directories to search for templates.
|
||||
"""
|
||||
# Immutable return value because it's cached and shared by callers.
|
||||
template_dirs = tuple(self.dirs)
|
||||
if self.app_dirs:
|
||||
template_dirs += get_app_template_dirs(self.app_dirname)
|
||||
return template_dirs
|
||||
|
||||
def iter_template_filenames(self, template_name):
|
||||
"""
|
||||
Iterate over candidate files for template_name.
|
||||
|
||||
Ignore files that don't lie inside configured template dirs to avoid
|
||||
directory traversal attacks.
|
||||
"""
|
||||
for template_dir in self.template_dirs:
|
||||
try:
|
||||
yield safe_join(template_dir, template_name)
|
||||
except SuspiciousFileOperation:
|
||||
# The joined path was located outside of this template_dir
|
||||
# (it might be inside another one, so this isn't fatal).
|
||||
pass
|
||||
@@ -0,0 +1,129 @@
|
||||
from importlib import import_module
|
||||
from pkgutil import walk_packages
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.template import TemplateDoesNotExist
|
||||
from django.template.context import make_context
|
||||
from django.template.engine import Engine
|
||||
from django.template.library import InvalidTemplateLibrary
|
||||
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class DjangoTemplates(BaseEngine):
|
||||
|
||||
app_dirname = 'templates'
|
||||
|
||||
def __init__(self, params):
|
||||
params = params.copy()
|
||||
options = params.pop('OPTIONS').copy()
|
||||
options.setdefault('autoescape', True)
|
||||
options.setdefault('debug', settings.DEBUG)
|
||||
options.setdefault('file_charset', settings.FILE_CHARSET)
|
||||
libraries = options.get('libraries', {})
|
||||
options['libraries'] = self.get_templatetag_libraries(libraries)
|
||||
super().__init__(params)
|
||||
self.engine = Engine(self.dirs, self.app_dirs, **options)
|
||||
|
||||
def from_string(self, template_code):
|
||||
return Template(self.engine.from_string(template_code), self)
|
||||
|
||||
def get_template(self, template_name):
|
||||
try:
|
||||
return Template(self.engine.get_template(template_name), self)
|
||||
except TemplateDoesNotExist as exc:
|
||||
reraise(exc, self)
|
||||
|
||||
def get_templatetag_libraries(self, custom_libraries):
|
||||
"""
|
||||
Return a collation of template tag libraries from installed
|
||||
applications and the supplied custom_libraries argument.
|
||||
"""
|
||||
libraries = get_installed_libraries()
|
||||
libraries.update(custom_libraries)
|
||||
return libraries
|
||||
|
||||
|
||||
class Template:
|
||||
|
||||
def __init__(self, template, backend):
|
||||
self.template = template
|
||||
self.backend = backend
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self.template.origin
|
||||
|
||||
def render(self, context=None, request=None):
|
||||
context = make_context(context, request, autoescape=self.backend.engine.autoescape)
|
||||
try:
|
||||
return self.template.render(context)
|
||||
except TemplateDoesNotExist as exc:
|
||||
reraise(exc, self.backend)
|
||||
|
||||
|
||||
def copy_exception(exc, backend=None):
|
||||
"""
|
||||
Create a new TemplateDoesNotExist. Preserve its declared attributes and
|
||||
template debug data but discard __traceback__, __context__, and __cause__
|
||||
to make this object suitable for keeping around (in a cache, for example).
|
||||
"""
|
||||
backend = backend or exc.backend
|
||||
new = exc.__class__(*exc.args, tried=exc.tried, backend=backend, chain=exc.chain)
|
||||
if hasattr(exc, 'template_debug'):
|
||||
new.template_debug = exc.template_debug
|
||||
return new
|
||||
|
||||
|
||||
def reraise(exc, backend):
|
||||
"""
|
||||
Reraise TemplateDoesNotExist while maintaining template debug information.
|
||||
"""
|
||||
new = copy_exception(exc, backend)
|
||||
raise new from exc
|
||||
|
||||
|
||||
def get_installed_libraries():
|
||||
"""
|
||||
Return the built-in template tag libraries and those from installed
|
||||
applications. Libraries are stored in a dictionary where keys are the
|
||||
individual module names, not the full module paths. Example:
|
||||
django.templatetags.i18n is stored as i18n.
|
||||
"""
|
||||
libraries = {}
|
||||
candidates = ['django.templatetags']
|
||||
candidates.extend(
|
||||
'%s.templatetags' % app_config.name
|
||||
for app_config in apps.get_app_configs())
|
||||
|
||||
for candidate in candidates:
|
||||
try:
|
||||
pkg = import_module(candidate)
|
||||
except ImportError:
|
||||
# No templatetags package defined. This is safe to ignore.
|
||||
continue
|
||||
|
||||
if hasattr(pkg, '__path__'):
|
||||
for name in get_package_libraries(pkg):
|
||||
libraries[name[len(candidate) + 1:]] = name
|
||||
|
||||
return libraries
|
||||
|
||||
|
||||
def get_package_libraries(pkg):
|
||||
"""
|
||||
Recursively yield template tag libraries defined in submodules of a
|
||||
package.
|
||||
"""
|
||||
for entry in walk_packages(pkg.__path__, pkg.__name__ + '.'):
|
||||
try:
|
||||
module = import_module(entry[1])
|
||||
except ImportError as e:
|
||||
raise InvalidTemplateLibrary(
|
||||
"Invalid template library specified. ImportError raised when "
|
||||
"trying to load '%s': %s" % (entry[1], e)
|
||||
)
|
||||
|
||||
if hasattr(module, 'register'):
|
||||
yield entry[1]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
File diff suppressed because it is too large.
Load diff
Whitespace-only changes.
@@ -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,46 @@
|
||||
"""
|
||||
Wrapper for loading templates from the filesystem.
|
||||
"""
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.template import Origin, TemplateDoesNotExist
|
||||
from django.utils._os import safe_join
|
||||
|
||||
from .base import Loader as BaseLoader
|
||||
|
||||
|
||||
class Loader(BaseLoader):
|
||||
|
||||
def __init__(self, engine, dirs=None):
|
||||
super().__init__(engine)
|
||||
self.dirs = dirs
|
||||
|
||||
def get_dirs(self):
|
||||
return self.dirs if self.dirs is not None else self.engine.dirs
|
||||
|
||||
def get_contents(self, origin):
|
||||
try:
|
||||
with open(origin.name, encoding=self.engine.file_charset) as fp:
|
||||
return fp.read()
|
||||
except FileNotFoundError:
|
||||
raise TemplateDoesNotExist(origin)
|
||||
|
||||
def get_template_sources(self, template_name):
|
||||
"""
|
||||
Return an Origin object pointing to an absolute path in each directory
|
||||
in template_dirs. For security reasons, if a path doesn't lie inside
|
||||
one of the template_dirs it is excluded from the result set.
|
||||
"""
|
||||
for template_dir in self.get_dirs():
|
||||
try:
|
||||
name = safe_join(template_dir, template_name)
|
||||
except SuspiciousFileOperation:
|
||||
# The joined path was located outside of this template_dir
|
||||
# (it might be inside another one, so this isn't fatal).
|
||||
continue
|
||||
|
||||
yield Origin(
|
||||
name=name,
|
||||
template_name=template_name,
|
||||
loader=self,
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .loader import get_template, select_template
|
||||
|
||||
|
||||
class ContentNotRenderedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SimpleTemplateResponse(HttpResponse):
|
||||
rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
|
||||
|
||||
def __init__(self, template, context=None, content_type=None, status=None,
|
||||
charset=None, using=None):
|
||||
# It would seem obvious to call these next two members 'template' and
|
||||
# 'context', but those names are reserved as part of the test Client
|
||||
# API. To avoid the name collision, we use different names.
|
||||
self.template_name = template
|
||||
self.context_data = context
|
||||
|
||||
self.using = using
|
||||
|
||||
self._post_render_callbacks = []
|
||||
|
||||
# _request stores the current request object in subclasses that know
|
||||
# about requests, like TemplateResponse. It's defined in the base class
|
||||
# to minimize code duplication.
|
||||
# It's called self._request because self.request gets overwritten by
|
||||
# django.test.client.Client. Unlike template_name and context_data,
|
||||
# _request should not be considered part of the public API.
|
||||
self._request = None
|
||||
|
||||
# content argument doesn't make sense here because it will be replaced
|
||||
# with rendered template so we always pass empty string in order to
|
||||
# prevent errors and provide shorter signature.
|
||||
super().__init__('', content_type, status, charset=charset)
|
||||
|
||||
# _is_rendered tracks whether the template and context has been baked
|
||||
# into a final response.
|
||||
# Super __init__ doesn't know any better than to set self.content to
|
||||
# the empty string we just gave it, which wrongly sets _is_rendered
|
||||
# True, so we initialize it to False after the call to super __init__.
|
||||
self._is_rendered = False
|
||||
|
||||
def __getstate__(self):
|
||||
"""
|
||||
Raise an exception if trying to pickle an unrendered response. Pickle
|
||||
only rendered data, not the data used to construct the response.
|
||||
"""
|
||||
obj_dict = self.__dict__.copy()
|
||||
if not self._is_rendered:
|
||||
raise ContentNotRenderedError('The response content must be '
|
||||
'rendered before it can be pickled.')
|
||||
for attr in self.rendering_attrs:
|
||||
if attr in obj_dict:
|
||||
del obj_dict[attr]
|
||||
|
||||
return obj_dict
|
||||
|
||||
def resolve_template(self, template):
|
||||
"""Accept a template object, path-to-template, or list of paths."""
|
||||
if isinstance(template, (list, tuple)):
|
||||
return select_template(template, using=self.using)
|
||||
elif isinstance(template, str):
|
||||
return get_template(template, using=self.using)
|
||||
else:
|
||||
return template
|
||||
|
||||
def resolve_context(self, context):
|
||||
return context
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
"""Return the freshly rendered content for the template and context
|
||||
described by the TemplateResponse.
|
||||
|
||||
This *does not* set the final content of the response. To set the
|
||||
response content, you must either call render(), or set the
|
||||
content explicitly using the value of this property.
|
||||
"""
|
||||
template = self.resolve_template(self.template_name)
|
||||
context = self.resolve_context(self.context_data)
|
||||
content = template.render(context, self._request)
|
||||
return content
|
||||
|
||||
def add_post_render_callback(self, callback):
|
||||
"""Add a new post-rendering callback.
|
||||
|
||||
If the response has already been rendered,
|
||||
invoke the callback immediately.
|
||||
"""
|
||||
if self._is_rendered:
|
||||
callback(self)
|
||||
else:
|
||||
self._post_render_callbacks.append(callback)
|
||||
|
||||
def render(self):
|
||||
"""Render (thereby finalizing) the content of the response.
|
||||
|
||||
If the content has already been rendered, this is a no-op.
|
||||
|
||||
Return the baked response instance.
|
||||
"""
|
||||
retval = self
|
||||
if not self._is_rendered:
|
||||
self.content = self.rendered_content
|
||||
for post_callback in self._post_render_callbacks:
|
||||
newretval = post_callback(retval)
|
||||
if newretval is not None:
|
||||
retval = newretval
|
||||
return retval
|
||||
|
||||
@property
|
||||
def is_rendered(self):
|
||||
return self._is_rendered
|
||||
|
||||
def __iter__(self):
|
||||
if not self._is_rendered:
|
||||
raise ContentNotRenderedError(
|
||||
'The response content must be rendered before it can be iterated over.'
|
||||
)
|
||||
return super().__iter__()
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
if not self._is_rendered:
|
||||
raise ContentNotRenderedError(
|
||||
'The response content must be rendered before it can be accessed.'
|
||||
)
|
||||
return super().content
|
||||
|
||||
@content.setter
|
||||
def content(self, value):
|
||||
"""Set the content for the response."""
|
||||
HttpResponse.content.fset(self, value)
|
||||
self._is_rendered = True
|
||||
|
||||
|
||||
class TemplateResponse(SimpleTemplateResponse):
|
||||
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']
|
||||
|
||||
def __init__(self, request, template, context=None, content_type=None,
|
||||
status=None, charset=None, using=None):
|
||||
super().__init__(template, context, content_type, status, charset, using)
|
||||
self._request = request
|
||||
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Parser and utilities for the smart 'if' tag
|
||||
"""
|
||||
# Using a simple top down parser, as described here:
|
||||
# http://effbot.org/zone/simple-top-down-parsing.htm.
|
||||
# 'led' = left denotation
|
||||
# 'nud' = null denotation
|
||||
# 'bp' = binding power (left = lbp, right = rbp)
|
||||
|
||||
|
||||
class TokenBase:
|
||||
"""
|
||||
Base class for operators and literals, mainly for debugging and for throwing
|
||||
syntax errors.
|
||||
"""
|
||||
id = None # node/token type name
|
||||
value = None # used by literals
|
||||
first = second = None # used by tree nodes
|
||||
|
||||
def nud(self, parser):
|
||||
# Null denotation - called in prefix context
|
||||
raise parser.error_class(
|
||||
"Not expecting '%s' in this position in if tag." % self.id
|
||||
)
|
||||
|
||||
def led(self, left, parser):
|
||||
# Left denotation - called in infix context
|
||||
raise parser.error_class(
|
||||
"Not expecting '%s' as infix operator in if tag." % self.id
|
||||
)
|
||||
|
||||
def display(self):
|
||||
"""
|
||||
Return what to display in error messages for this node
|
||||
"""
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
|
||||
return "(" + " ".join(out) + ")"
|
||||
|
||||
|
||||
def infix(bp, func):
|
||||
"""
|
||||
Create an infix operator, given a binding power and a function that
|
||||
evaluates the node.
|
||||
"""
|
||||
class Operator(TokenBase):
|
||||
lbp = bp
|
||||
|
||||
def led(self, left, parser):
|
||||
self.first = left
|
||||
self.second = parser.expression(bp)
|
||||
return self
|
||||
|
||||
def eval(self, context):
|
||||
try:
|
||||
return func(context, self.first, self.second)
|
||||
except Exception:
|
||||
# Templates shouldn't throw exceptions when rendering. We are
|
||||
# most likely to get exceptions for things like {% if foo in bar
|
||||
# %} where 'bar' does not support 'in', so default to False
|
||||
return False
|
||||
|
||||
return Operator
|
||||
|
||||
|
||||
def prefix(bp, func):
|
||||
"""
|
||||
Create a prefix operator, given a binding power and a function that
|
||||
evaluates the node.
|
||||
"""
|
||||
class Operator(TokenBase):
|
||||
lbp = bp
|
||||
|
||||
def nud(self, parser):
|
||||
self.first = parser.expression(bp)
|
||||
self.second = None
|
||||
return self
|
||||
|
||||
def eval(self, context):
|
||||
try:
|
||||
return func(context, self.first)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return Operator
|
||||
|
||||
|
||||
# Operator precedence follows Python.
|
||||
# We defer variable evaluation to the lambda to ensure that terms are
|
||||
# lazily evaluated using Python's boolean parsing logic.
|
||||
OPERATORS = {
|
||||
'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
|
||||
'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
|
||||
'not': prefix(8, lambda context, x: not x.eval(context)),
|
||||
'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
|
||||
'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
|
||||
'is': infix(10, lambda context, x, y: x.eval(context) is y.eval(context)),
|
||||
'is not': infix(10, lambda context, x, y: x.eval(context) is not y.eval(context)),
|
||||
'==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
|
||||
'!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
|
||||
'>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
|
||||
'>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
|
||||
'<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
|
||||
'<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
|
||||
}
|
||||
|
||||
# Assign 'id' to each:
|
||||
for key, op in OPERATORS.items():
|
||||
op.id = key
|
||||
|
||||
|
||||
class Literal(TokenBase):
|
||||
"""
|
||||
A basic self-resolvable object similar to a Django template variable.
|
||||
"""
|
||||
# IfParser uses Literal in create_var, but TemplateIfParser overrides
|
||||
# create_var so that a proper implementation that actually resolves
|
||||
# variables, filters etc. is used.
|
||||
id = "literal"
|
||||
lbp = 0
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def display(self):
|
||||
return repr(self.value)
|
||||
|
||||
def nud(self, parser):
|
||||
return self
|
||||
|
||||
def eval(self, context):
|
||||
return self.value
|
||||
|
||||
def __repr__(self):
|
||||
return "(%s %r)" % (self.id, self.value)
|
||||
|
||||
|
||||
class EndToken(TokenBase):
|
||||
lbp = 0
|
||||
|
||||
def nud(self, parser):
|
||||
raise parser.error_class("Unexpected end of expression in if tag.")
|
||||
|
||||
|
||||
EndToken = EndToken()
|
||||
|
||||
|
||||
class IfParser:
|
||||
error_class = ValueError
|
||||
|
||||
def __init__(self, tokens):
|
||||
# Turn 'is','not' and 'not','in' into single tokens.
|
||||
num_tokens = len(tokens)
|
||||
mapped_tokens = []
|
||||
i = 0
|
||||
while i < num_tokens:
|
||||
token = tokens[i]
|
||||
if token == "is" and i + 1 < num_tokens and tokens[i + 1] == "not":
|
||||
token = "is not"
|
||||
i += 1 # skip 'not'
|
||||
elif token == "not" and i + 1 < num_tokens and tokens[i + 1] == "in":
|
||||
token = "not in"
|
||||
i += 1 # skip 'in'
|
||||
mapped_tokens.append(self.translate_token(token))
|
||||
i += 1
|
||||
|
||||
self.tokens = mapped_tokens
|
||||
self.pos = 0
|
||||
self.current_token = self.next_token()
|
||||
|
||||
def translate_token(self, token):
|
||||
try:
|
||||
op = OPERATORS[token]
|
||||
except (KeyError, TypeError):
|
||||
return self.create_var(token)
|
||||
else:
|
||||
return op()
|
||||
|
||||
def next_token(self):
|
||||
if self.pos >= len(self.tokens):
|
||||
return EndToken
|
||||
else:
|
||||
retval = self.tokens[self.pos]
|
||||
self.pos += 1
|
||||
return retval
|
||||
|
||||
def parse(self):
|
||||
retval = self.expression()
|
||||
# Check that we have exhausted all the tokens
|
||||
if self.current_token is not EndToken:
|
||||
raise self.error_class("Unused '%s' at end of if expression." %
|
||||
self.current_token.display())
|
||||
return retval
|
||||
|
||||
def expression(self, rbp=0):
|
||||
t = self.current_token
|
||||
self.current_token = self.next_token()
|
||||
left = t.nud(self)
|
||||
while rbp < self.current_token.lbp:
|
||||
t = self.current_token
|
||||
self.current_token = self.next_token()
|
||||
left = t.led(left, self)
|
||||
return left
|
||||
|
||||
def create_var(self, value):
|
||||
return Literal(value)
|
||||
Reference in new issue
Block a user