Fixed #17085, #24783 -- Refactored template library registration.

* Converted the ``libraries`` and ``builtins`` globals of
  ``django.template.base`` into properties of the Engine class.
* Added a public API for explicit registration of libraries and builtins.
This commit is contained in:
Preston Timmons 2015-05-08 15:10:36 -05:00
parent 7b8008a078
commit 655f524915
49 changed files with 949 additions and 594 deletions

View File

@ -12,12 +12,7 @@ from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.db import models
from django.http import Http404
from django.template.base import (
InvalidTemplateLibrary, builtins, get_library, get_templatetags_modules,
libraries,
)
from django.template.engine import Engine
from django.utils._os import upath
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
@ -60,11 +55,15 @@ class TemplateTagIndexView(BaseAdminDocsView):
template_name = 'admin_doc/template_tag_index.html'
def get_context_data(self, **kwargs):
load_all_installed_template_libraries()
tags = []
app_libs = list(libraries.items())
builtin_libs = [(None, lib) for lib in builtins]
try:
engine = Engine.get_default()
except ImproperlyConfigured:
# Non-trivial TEMPLATES settings aren't supported (#24125).
pass
else:
app_libs = sorted(engine.template_libraries.items())
builtin_libs = [('', lib) for lib in engine.template_builtins]
for module_name, library in builtin_libs + app_libs:
for tag_name, tag_func in library.tags.items():
title, body, metadata = utils.parse_docstring(tag_func.__doc__)
@ -74,9 +73,6 @@ class TemplateTagIndexView(BaseAdminDocsView):
body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
if library in builtins:
tag_library = ''
else:
tag_library = module_name.split('.')[-1]
tags.append({
'name': tag_name,
@ -93,11 +89,15 @@ class TemplateFilterIndexView(BaseAdminDocsView):
template_name = 'admin_doc/template_filter_index.html'
def get_context_data(self, **kwargs):
load_all_installed_template_libraries()
filters = []
app_libs = list(libraries.items())
builtin_libs = [(None, lib) for lib in builtins]
try:
engine = Engine.get_default()
except ImproperlyConfigured:
# Non-trivial TEMPLATES settings aren't supported (#24125).
pass
else:
app_libs = sorted(engine.template_libraries.items())
builtin_libs = [('', lib) for lib in engine.template_builtins]
for module_name, library in builtin_libs + app_libs:
for filter_name, filter_func in library.filters.items():
title, body, metadata = utils.parse_docstring(filter_func.__doc__)
@ -107,9 +107,6 @@ class TemplateFilterIndexView(BaseAdminDocsView):
body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
if library in builtins:
tag_library = ''
else:
tag_library = module_name.split('.')[-1]
filters.append({
'name': filter_name,
@ -320,29 +317,6 @@ class TemplateDetailView(BaseAdminDocsView):
# Helper functions #
####################
def load_all_installed_template_libraries():
# Load/register all template tag libraries from installed apps.
for module_name in get_templatetags_modules():
mod = import_module(module_name)
if not hasattr(mod, '__file__'):
# e.g. packages installed as eggs
continue
try:
libraries = [
os.path.splitext(p)[0]
for p in os.listdir(os.path.dirname(upath(mod.__file__)))
if p.endswith('.py') and p[0].isalpha()
]
except OSError:
continue
else:
for library_name in libraries:
try:
get_library(library_name)
except InvalidTemplateLibrary:
pass
def get_return_data_type(func_name):
"""Return a somewhat-helpful data type given a function name"""

View File

@ -66,7 +66,7 @@ from .base import (Context, Node, NodeList, Origin, RequestContext, # NOQA
from .base import resolve_variable # NOQA
# Library management
from .base import Library # NOQA
from .library import Library # NOQA
__all__ += ('Template', 'Context', 'RequestContext')

View File

@ -3,11 +3,15 @@ from __future__ import absolute_import
import sys
import warnings
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 Context, RequestContext, make_context
from django.template.engine import Engine, _dirs_undefined
from django.template.library import InvalidTemplateLibrary
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
@ -23,6 +27,8 @@ class DjangoTemplates(BaseEngine):
options = params.pop('OPTIONS').copy()
options.setdefault('debug', settings.DEBUG)
options.setdefault('file_charset', settings.FILE_CHARSET)
libraries = options.get('libraries', {})
options['libraries'] = self.get_templatetag_libraries(libraries)
super(DjangoTemplates, self).__init__(params)
self.engine = Engine(self.dirs, self.app_dirs, **options)
@ -35,6 +41,15 @@ class DjangoTemplates(BaseEngine):
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(object):
@ -90,3 +105,48 @@ def reraise(exc, backend):
if hasattr(exc, 'template_debug'):
new.template_debug = exc.template_debug
six.reraise(exc.__class__, new, sys.exc_info()[2])
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]

View File

@ -54,25 +54,18 @@ from __future__ import unicode_literals
import logging
import re
import warnings
from functools import partial
from importlib import import_module
from inspect import getargspec, getcallargs
from django.apps import apps
from django.template.context import ( # NOQA: imported for backwards compatibility
BaseContext, Context, ContextPopException, RequestContext,
)
from django.utils import lru_cache, six
from django.utils.deprecation import (
RemovedInDjango20Warning, RemovedInDjango21Warning,
)
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import (
force_str, force_text, python_2_unicode_compatible,
)
from django.utils.formats import localize
from django.utils.html import conditional_escape, escape
from django.utils.itercompat import is_iterable
from django.utils.module_loading import module_has_submodule
from django.utils.safestring import (
EscapeData, SafeData, mark_for_escaping, mark_safe,
)
@ -123,11 +116,6 @@ tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' %
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END))))
# global dictionary of libraries that have been loaded using get_library
libraries = {}
# global list of libraries to load by default for a new parser
builtins = []
logger = logging.getLogger('django.template')
@ -146,10 +134,6 @@ class VariableDoesNotExist(Exception):
return self.msg % tuple(force_text(p, errors='replace') for p in self.params)
class InvalidTemplateLibrary(Exception):
pass
class Origin(object):
def __init__(self, name, template_name=None, loader=None):
self.name = name
@ -232,7 +216,9 @@ class Template(object):
lexer = Lexer(self.source)
tokens = lexer.tokenize()
parser = Parser(tokens)
parser = Parser(
tokens, self.engine.template_libraries, self.engine.template_builtins,
)
try:
return parser.parse()
@ -452,13 +438,20 @@ class DebugLexer(Lexer):
class Parser(object):
def __init__(self, tokens):
def __init__(self, tokens, libraries=None, builtins=None):
self.tokens = tokens
self.tags = {}
self.filters = {}
self.command_stack = []
for lib in builtins:
self.add_library(lib)
if libraries is None:
libraries = {}
if builtins is None:
builtins = []
self.libraries = libraries
for builtin in builtins:
self.add_library(builtin)
def parse(self, parse_until=None):
"""
@ -1073,377 +1066,3 @@ def token_kwargs(bits, parser, support_legacy=False):
return kwargs
del bits[:1]
return kwargs
def parse_bits(parser, bits, params, varargs, varkw, defaults,
takes_context, name):
"""
Parses bits for template tag helpers simple_tag and inclusion_tag, in
particular by detecting syntax errors and by extracting positional and
keyword arguments.
"""
if takes_context:
if params[0] == 'context':
params = params[1:]
else:
raise TemplateSyntaxError(
"'%s' is decorated with takes_context=True so it must "
"have a first argument of 'context'" % name)
args = []
kwargs = {}
unhandled_params = list(params)
for bit in bits:
# First we try to extract a potential kwarg from the bit
kwarg = token_kwargs([bit], parser)
if kwarg:
# The kwarg was successfully extracted
param, value = kwarg.popitem()
if param not in params and varkw is None:
# An unexpected keyword argument was supplied
raise TemplateSyntaxError(
"'%s' received unexpected keyword argument '%s'" %
(name, param))
elif param in kwargs:
# The keyword argument has already been supplied once
raise TemplateSyntaxError(
"'%s' received multiple values for keyword argument '%s'" %
(name, param))
else:
# All good, record the keyword argument
kwargs[str(param)] = value
if param in unhandled_params:
# If using the keyword syntax for a positional arg, then
# consume it.
unhandled_params.remove(param)
else:
if kwargs:
raise TemplateSyntaxError(
"'%s' received some positional argument(s) after some "
"keyword argument(s)" % name)
else:
# Record the positional argument
args.append(parser.compile_filter(bit))
try:
# Consume from the list of expected positional arguments
unhandled_params.pop(0)
except IndexError:
if varargs is None:
raise TemplateSyntaxError(
"'%s' received too many positional arguments" %
name)
if defaults is not None:
# Consider the last n params handled, where n is the
# number of defaults.
unhandled_params = unhandled_params[:-len(defaults)]
if unhandled_params:
# Some positional arguments were not supplied
raise TemplateSyntaxError(
"'%s' did not receive value(s) for the argument(s): %s" %
(name, ", ".join("'%s'" % p for p in unhandled_params)))
return args, kwargs
def generic_tag_compiler(parser, token, params, varargs, varkw, defaults,
name, takes_context, node_class):
"""
Returns a template.Node subclass.
"""
bits = token.split_contents()[1:]
args, kwargs = parse_bits(parser, bits, params, varargs, varkw,
defaults, takes_context, name)
return node_class(takes_context, args, kwargs)
class TagHelperNode(Node):
"""
Base class for tag helper nodes such as SimpleNode and InclusionNode.
Manages the positional and keyword arguments to be passed to the decorated
function.
"""
def __init__(self, takes_context, args, kwargs):
self.takes_context = takes_context
self.args = args
self.kwargs = kwargs
def get_resolved_arguments(self, context):
resolved_args = [var.resolve(context) for var in self.args]
if self.takes_context:
resolved_args = [context] + resolved_args
resolved_kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
return resolved_args, resolved_kwargs
class Library(object):
def __init__(self):
self.filters = {}
self.tags = {}
def tag(self, name=None, compile_function=None):
if name is None and compile_function is None:
# @register.tag()
return self.tag_function
elif name is not None and compile_function is None:
if callable(name):
# @register.tag
return self.tag_function(name)
else:
# @register.tag('somename') or @register.tag(name='somename')
def dec(func):
return self.tag(name, func)
return dec
elif name is not None and compile_function is not None:
# register.tag('somename', somefunc)
self.tags[name] = compile_function
return compile_function
else:
raise InvalidTemplateLibrary("Unsupported arguments to "
"Library.tag: (%r, %r)", (name, compile_function))
def tag_function(self, func):
self.tags[getattr(func, "_decorated_function", func).__name__] = func
return func
def filter(self, name=None, filter_func=None, **flags):
if name is None and filter_func is None:
# @register.filter()
def dec(func):
return self.filter_function(func, **flags)
return dec
elif name is not None and filter_func is None:
if callable(name):
# @register.filter
return self.filter_function(name, **flags)
else:
# @register.filter('somename') or @register.filter(name='somename')
def dec(func):
return self.filter(name, func, **flags)
return dec
elif name is not None and filter_func is not None:
# register.filter('somename', somefunc)
self.filters[name] = filter_func
for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'):
if attr in flags:
value = flags[attr]
# set the flag on the filter for FilterExpression.resolve
setattr(filter_func, attr, value)
# set the flag on the innermost decorated function
# for decorators that need it e.g. stringfilter
if hasattr(filter_func, "_decorated_function"):
setattr(filter_func._decorated_function, attr, value)
filter_func._filter_name = name
return filter_func
else:
raise InvalidTemplateLibrary("Unsupported arguments to "
"Library.filter: (%r, %r)", (name, filter_func))
def filter_function(self, func, **flags):
name = getattr(func, "_decorated_function", func).__name__
return self.filter(name, func, **flags)
def simple_tag(self, func=None, takes_context=None, name=None):
def dec(func):
params, varargs, varkw, defaults = getargspec(func)
class SimpleNode(TagHelperNode):
def __init__(self, takes_context, args, kwargs, target_var):
super(SimpleNode, self).__init__(takes_context, args, kwargs)
self.target_var = target_var
def render(self, context):
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
output = func(*resolved_args, **resolved_kwargs)
if self.target_var is not None:
context[self.target_var] = output
return ''
return output
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
def compile_func(parser, token):
bits = token.split_contents()[1:]
target_var = None
if len(bits) >= 2 and bits[-2] == 'as':
target_var = bits[-1]
bits = bits[:-2]
args, kwargs = parse_bits(parser, bits, params,
varargs, varkw, defaults, takes_context, function_name)
return SimpleNode(takes_context, args, kwargs, target_var)
compile_func.__doc__ = func.__doc__
self.tag(function_name, compile_func)
return func
if func is None:
# @register.simple_tag(...)
return dec
elif callable(func):
# @register.simple_tag
return dec(func)
else:
raise TemplateSyntaxError("Invalid arguments provided to simple_tag")
def assignment_tag(self, func=None, takes_context=None, name=None):
warnings.warn(
"assignment_tag() is deprecated. Use simple_tag() instead",
RemovedInDjango21Warning,
stacklevel=2,
)
return self.simple_tag(func, takes_context, name)
def inclusion_tag(self, file_name, takes_context=False, name=None):
def dec(func):
params, varargs, varkw, defaults = getargspec(func)
class InclusionNode(TagHelperNode):
def render(self, context):
"""
Renders the specified template and context. Caches the
template object in render_context to avoid reparsing and
loading when used in a for loop.
"""
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
_dict = func(*resolved_args, **resolved_kwargs)
t = context.render_context.get(self)
if t is None:
if isinstance(file_name, Template):
t = file_name
elif isinstance(getattr(file_name, 'template', None), Template):
t = file_name.template
elif not isinstance(file_name, six.string_types) and is_iterable(file_name):
t = context.template.engine.select_template(file_name)
else:
t = context.template.engine.get_template(file_name)
context.render_context[self] = t
new_context = context.new(_dict)
# Copy across the CSRF token, if present, because
# inclusion tags are often used for forms, and we need
# instructions for using CSRF protection to be as simple
# as possible.
csrf_token = context.get('csrf_token')
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return t.render(new_context)
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
compile_func = partial(generic_tag_compiler,
params=params, varargs=varargs, varkw=varkw,
defaults=defaults, name=function_name,
takes_context=takes_context, node_class=InclusionNode)
compile_func.__doc__ = func.__doc__
self.tag(function_name, compile_func)
return func
return dec
def is_library_missing(name):
"""Check if library that failed to load cannot be found under any
templatetags directory or does exist but fails to import.
Non-existing condition is checked recursively for each subpackage in cases
like <appdir>/templatetags/subpackage/package/module.py.
"""
# Don't bother to check if '.' is in name since any name will be prefixed
# with some template root.
path, module = name.rsplit('.', 1)
try:
package = import_module(path)
return not module_has_submodule(package, module)
except ImportError:
return is_library_missing(path)
def import_library(taglib_module):
"""
Load a template tag library module.
Verifies that the library contains a 'register' attribute, and
returns that attribute as the representation of the library
"""
try:
mod = import_module(taglib_module)
except ImportError as e:
# If the ImportError is because the taglib submodule does not exist,
# that's not an error that should be raised. If the submodule exists
# and raised an ImportError on the attempt to load it, that we want
# to raise.
if is_library_missing(taglib_module):
return None
else:
raise InvalidTemplateLibrary("ImportError raised loading %s: %s" %
(taglib_module, e))
try:
return mod.register
except AttributeError:
raise InvalidTemplateLibrary("Template library %s does not have "
"a variable named 'register'" %
taglib_module)
@lru_cache.lru_cache()
def get_templatetags_modules():
"""
Return the list of all available template tag modules.
Caches the result for faster access.
"""
templatetags_modules_candidates = ['django.templatetags']
templatetags_modules_candidates.extend(
'%s.templatetags' % app_config.name
for app_config in apps.get_app_configs())
templatetags_modules = []
for templatetag_module in templatetags_modules_candidates:
try:
import_module(templatetag_module)
except ImportError:
continue
else:
templatetags_modules.append(templatetag_module)
return templatetags_modules
def get_library(library_name):
"""
Load the template library module with the given name.
If library is not already loaded loop over all templatetags modules
to locate it.
{% load somelib %} and {% load someotherlib %} loops twice.
Subsequent loads eg. {% load somelib %} in the same process will grab
the cached module from libraries.
"""
lib = libraries.get(library_name)
if not lib:
templatetags_modules = get_templatetags_modules()
tried_modules = []
for module in templatetags_modules:
taglib_module = '%s.%s' % (module, library_name)
tried_modules.append(taglib_module)
lib = import_library(taglib_module)
if lib:
libraries[library_name] = lib
break
if not lib:
raise InvalidTemplateLibrary("Template library %s not found, "
"tried %s" %
(library_name,
','.join(tried_modules)))
return lib
def add_to_builtins(module):
builtins.append(import_library(module))
add_to_builtins('django.template.defaulttags')
add_to_builtins('django.template.defaultfilters')
add_to_builtins('django.template.loader_tags')

View File

@ -25,7 +25,8 @@ from django.utils.text import (
from django.utils.timesince import timesince, timeuntil
from django.utils.translation import ugettext, ungettext
from .base import Library, Variable, VariableDoesNotExist
from .base import Variable, VariableDoesNotExist
from .library import Library
register = Library()

View File

@ -19,12 +19,12 @@ from django.utils.safestring import mark_safe
from .base import (
BLOCK_TAG_END, BLOCK_TAG_START, COMMENT_TAG_END, COMMENT_TAG_START,
SINGLE_BRACE_END, SINGLE_BRACE_START, VARIABLE_ATTRIBUTE_SEPARATOR,
VARIABLE_TAG_END, VARIABLE_TAG_START, Context, InvalidTemplateLibrary,
Library, Node, NodeList, Template, TemplateSyntaxError,
VariableDoesNotExist, get_library, kwarg_re, render_value_in_context,
token_kwargs,
VARIABLE_TAG_END, VARIABLE_TAG_START, Context, Node, NodeList, Template,
TemplateSyntaxError, VariableDoesNotExist, kwarg_re,
render_value_in_context, token_kwargs,
)
from .defaultfilters import date
from .library import Library
from .smartif import IfParser, Literal
register = Library()
@ -1121,10 +1121,43 @@ def ssi(parser, token):
return SsiNode(filepath, parsed)
def find_library(parser, name):
try:
return parser.libraries[name]
except KeyError:
raise TemplateSyntaxError(
"'%s' is not a registered tag library. Must be one of:\n%s" % (
name, "\n".join(sorted(parser.libraries.keys())),
),
)
def load_from_library(library, label, names):
"""
Return a subset of tags and filters from a library.
"""
subset = Library()
for name in names:
found = False
if name in library.tags:
found = True
subset.tags[name] = library.tags[name]
if name in library.filters:
found = True
subset.filters[name] = library.filters[name]
if found is False:
raise TemplateSyntaxError(
"'%s' is not a valid tag or filter in tag library '%s'" % (
name, label,
),
)
return subset
@register.tag
def load(parser, token):
"""
Loads a custom template tag set.
Loads a custom template tag library into the parser.
For example, to load the template tags in
``django/templatetags/news/photos.py``::
@ -1140,35 +1173,16 @@ def load(parser, token):
# token.split_contents() isn't useful here because this tag doesn't accept variable as arguments
bits = token.contents.split()
if len(bits) >= 4 and bits[-2] == "from":
try:
taglib = bits[-1]
lib = get_library(taglib)
except InvalidTemplateLibrary as e:
raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
(taglib, e))
# from syntax is used; load individual tags from the library
name = bits[-1]
lib = find_library(parser, name)
subset = load_from_library(lib, name, bits[1:-2])
parser.add_library(subset)
else:
temp_lib = Library()
for name in bits[1:-2]:
if name in lib.tags:
temp_lib.tags[name] = lib.tags[name]
# a name could be a tag *and* a filter, so check for both
if name in lib.filters:
temp_lib.filters[name] = lib.filters[name]
elif name in lib.filters:
temp_lib.filters[name] = lib.filters[name]
else:
raise TemplateSyntaxError("'%s' is not a valid tag or filter in tag library '%s'" %
(name, taglib))
parser.add_library(temp_lib)
else:
for taglib in bits[1:]:
# add the library to the parser
try:
lib = get_library(taglib)
# one or more libraries are specified; load and add them to the parser
for name in bits[1:]:
lib = find_library(parser, name)
parser.add_library(lib)
except InvalidTemplateLibrary as e:
raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
(taglib, e))
return LoadNode()

View File

@ -9,6 +9,7 @@ from django.utils.module_loading import import_string
from .base import Context, Template
from .context import _builtin_context_processors
from .exceptions import TemplateDoesNotExist
from .library import import_library
_context_instance_undefined = object()
_dictionary_undefined = object()
@ -16,11 +17,16 @@ _dirs_undefined = object()
class Engine(object):
default_builtins = [
'django.template.defaulttags',
'django.template.defaultfilters',
'django.template.loader_tags',
]
def __init__(self, dirs=None, app_dirs=False,
allowed_include_roots=None, context_processors=None,
debug=False, loaders=None, string_if_invalid='',
file_charset='utf-8'):
file_charset='utf-8', libraries=None, builtins=None):
if dirs is None:
dirs = []
if allowed_include_roots is None:
@ -35,6 +41,10 @@ class Engine(object):
if app_dirs:
raise ImproperlyConfigured(
"app_dirs must not be set when loaders is defined.")
if libraries is None:
libraries = {}
if builtins is None:
builtins = []
if isinstance(allowed_include_roots, six.string_types):
raise ImproperlyConfigured(
@ -48,6 +58,10 @@ class Engine(object):
self.loaders = loaders
self.string_if_invalid = string_if_invalid
self.file_charset = file_charset
self.libraries = libraries
self.template_libraries = self.get_template_libraries(libraries)
self.builtins = self.default_builtins + builtins
self.template_builtins = self.get_template_builtins(self.builtins)
@staticmethod
@lru_cache.lru_cache()
@ -90,6 +104,15 @@ class Engine(object):
context_processors += tuple(self.context_processors)
return tuple(import_string(path) for path in context_processors)
def get_template_builtins(self, builtins):
return [import_library(x) for x in builtins]
def get_template_libraries(self, libraries):
loaded = {}
for name, path in libraries.items():
loaded[name] = import_library(path)
return loaded
@cached_property
def template_loaders(self):
return self.get_template_loaders(self.loaders)

327
django/template/library.py Normal file
View File

@ -0,0 +1,327 @@
import functools
import warnings
from importlib import import_module
from inspect import getargspec
from django.utils import six
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.itercompat import is_iterable
from .base import Node, Template, token_kwargs
from .exceptions import TemplateSyntaxError
class InvalidTemplateLibrary(Exception):
pass
class Library(object):
"""
A class for registering template tags and filters. Compiled filter and
template tag functions are stored in the filters and tags attributes.
The filter, simple_tag, and inclusion_tag methods provide a convenient
way to register callables as tags.
"""
def __init__(self):
self.filters = {}
self.tags = {}
def tag(self, name=None, compile_function=None):
if name is None and compile_function is None:
# @register.tag()
return self.tag_function
elif name is not None and compile_function is None:
if callable(name):
# @register.tag
return self.tag_function(name)
else:
# @register.tag('somename') or @register.tag(name='somename')
def dec(func):
return self.tag(name, func)
return dec
elif name is not None and compile_function is not None:
# register.tag('somename', somefunc)
self.tags[name] = compile_function
return compile_function
else:
raise ValueError(
"Unsupported arguments to Library.tag: (%r, %r)" %
(name, compile_function),
)
def tag_function(self, func):
self.tags[getattr(func, "_decorated_function", func).__name__] = func
return func
def filter(self, name=None, filter_func=None, **flags):
"""
Register a callable as a template filter. Example:
@register.filter
def lower(value):
return value.lower()
"""
if name is None and filter_func is None:
# @register.filter()
def dec(func):
return self.filter_function(func, **flags)
return dec
elif name is not None and filter_func is None:
if callable(name):
# @register.filter
return self.filter_function(name, **flags)
else:
# @register.filter('somename') or @register.filter(name='somename')
def dec(func):
return self.filter(name, func, **flags)
return dec
elif name is not None and filter_func is not None:
# register.filter('somename', somefunc)
self.filters[name] = filter_func
for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'):
if attr in flags:
value = flags[attr]
# set the flag on the filter for FilterExpression.resolve
setattr(filter_func, attr, value)
# set the flag on the innermost decorated function
# for decorators that need it, e.g. stringfilter
if hasattr(filter_func, "_decorated_function"):
setattr(filter_func._decorated_function, attr, value)
filter_func._filter_name = name
return filter_func
else:
raise ValueError(
"Unsupported arguments to Library.filter: (%r, %r)" %
(name, filter_func),
)
def filter_function(self, func, **flags):
name = getattr(func, "_decorated_function", func).__name__
return self.filter(name, func, **flags)
def simple_tag(self, func=None, takes_context=None, name=None):
"""
Register a callable as a compiled template tag. Example:
@register.simple_tag
def hello(*args, **kwargs):
return 'world'
"""
def dec(func):
params, varargs, varkw, defaults = getargspec(func)
function_name = (name or getattr(func, '_decorated_function', func).__name__)
@functools.wraps(func)
def compile_func(parser, token):
bits = token.split_contents()[1:]
target_var = None
if len(bits) >= 2 and bits[-2] == 'as':
target_var = bits[-1]
bits = bits[:-2]
args, kwargs = parse_bits(parser, bits, params,
varargs, varkw, defaults, takes_context, function_name)
return SimpleNode(func, takes_context, args, kwargs, target_var)
self.tag(function_name, compile_func)
return func
if func is None:
# @register.simple_tag(...)
return dec
elif callable(func):
# @register.simple_tag
return dec(func)
else:
raise ValueError("Invalid arguments provided to simple_tag")
def assignment_tag(self, func=None, takes_context=None, name=None):
warnings.warn(
"assignment_tag() is deprecated. Use simple_tag() instead",
RemovedInDjango21Warning,
stacklevel=2,
)
return self.simple_tag(func, takes_context, name)
def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
"""
Register a callable as an inclusion tag:
@register.inclusion_tag('results.html')
def show_results(poll):
choices = poll.choice_set.all()
return {'choices': choices}
"""
def dec(func):
params, varargs, varkw, defaults = getargspec(func)
function_name = (name or getattr(func, '_decorated_function', func).__name__)
@functools.wraps(func)
def compile_func(parser, token):
bits = token.split_contents()[1:]
args, kwargs = parse_bits(
parser, bits, params, varargs, varkw, defaults,
takes_context, function_name,
)
return InclusionNode(
func, takes_context, args, kwargs, filename,
)
self.tag(function_name, compile_func)
return func
return dec
class TagHelperNode(Node):
"""
Base class for tag helper nodes such as SimpleNode and InclusionNode.
Manages the positional and keyword arguments to be passed to the decorated
function.
"""
def __init__(self, func, takes_context, args, kwargs):
self.func = func
self.takes_context = takes_context
self.args = args
self.kwargs = kwargs
def get_resolved_arguments(self, context):
resolved_args = [var.resolve(context) for var in self.args]
if self.takes_context:
resolved_args = [context] + resolved_args
resolved_kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
return resolved_args, resolved_kwargs
class SimpleNode(TagHelperNode):
def __init__(self, func, takes_context, args, kwargs, target_var):
super(SimpleNode, self).__init__(func, takes_context, args, kwargs)
self.target_var = target_var
def render(self, context):
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
output = self.func(*resolved_args, **resolved_kwargs)
if self.target_var is not None:
context[self.target_var] = output
return ''
return output
class InclusionNode(TagHelperNode):
def __init__(self, func, takes_context, args, kwargs, filename):
super(InclusionNode, self).__init__(func, takes_context, args, kwargs)
self.filename = filename
def render(self, context):
"""
Render the specified template and context. Cache the template object
in render_context to avoid reparsing and loading when used in a for
loop.
"""
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
_dict = self.func(*resolved_args, **resolved_kwargs)
t = context.render_context.get(self)
if t is None:
if isinstance(self.filename, Template):
t = self.filename
elif isinstance(getattr(self.filename, 'template', None), Template):
t = self.filename.template
elif not isinstance(self.filename, six.string_types) and is_iterable(self.filename):
t = context.template.engine.select_template(self.filename)
else:
t = context.template.engine.get_template(self.filename)
context.render_context[self] = t
new_context = context.new(_dict)
# Copy across the CSRF token, if present, because inclusion tags are
# often used for forms, and we need instructions for using CSRF
# protection to be as simple as possible.
csrf_token = context.get('csrf_token')
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return t.render(new_context)
def parse_bits(parser, bits, params, varargs, varkw, defaults,
takes_context, name):
"""
Parse bits for template tag helpers simple_tag and inclusion_tag, in
particular by detecting syntax errors and by extracting positional and
keyword arguments.
"""
if takes_context:
if params[0] == 'context':
params = params[1:]
else:
raise TemplateSyntaxError(
"'%s' is decorated with takes_context=True so it must "
"have a first argument of 'context'" % name)
args = []
kwargs = {}
unhandled_params = list(params)
for bit in bits:
# First we try to extract a potential kwarg from the bit
kwarg = token_kwargs([bit], parser)
if kwarg:
# The kwarg was successfully extracted
param, value = kwarg.popitem()
if param not in params and varkw is None:
# An unexpected keyword argument was supplied
raise TemplateSyntaxError(
"'%s' received unexpected keyword argument '%s'" %
(name, param))
elif param in kwargs:
# The keyword argument has already been supplied once
raise TemplateSyntaxError(
"'%s' received multiple values for keyword argument '%s'" %
(name, param))
else:
# All good, record the keyword argument
kwargs[str(param)] = value
if param in unhandled_params:
# If using the keyword syntax for a positional arg, then
# consume it.
unhandled_params.remove(param)
else:
if kwargs:
raise TemplateSyntaxError(
"'%s' received some positional argument(s) after some "
"keyword argument(s)" % name)
else:
# Record the positional argument
args.append(parser.compile_filter(bit))
try:
# Consume from the list of expected positional arguments
unhandled_params.pop(0)
except IndexError:
if varargs is None:
raise TemplateSyntaxError(
"'%s' received too many positional arguments" %
name)
if defaults is not None:
# Consider the last n params handled, where n is the
# number of defaults.
unhandled_params = unhandled_params[:-len(defaults)]
if unhandled_params:
# Some positional arguments were not supplied
raise TemplateSyntaxError(
"'%s' did not receive value(s) for the argument(s): %s" %
(name, ", ".join("'%s'" % p for p in unhandled_params)))
return args, kwargs
def import_library(name):
"""
Load a Library object from a template tag module.
"""
try:
module = import_module(name)
except ImportError as e:
raise InvalidTemplateLibrary(
"Invalid template library specified. ImportError raised when "
"trying to load '%s': %s" % (name, e)
)
try:
return module.register
except AttributeError:
raise InvalidTemplateLibrary(
"Module %s does not have a variable named 'register'" % name,
)

View File

@ -4,9 +4,9 @@ from django.utils import six
from django.utils.safestring import mark_safe
from .base import (
Library, Node, Template, TemplateSyntaxError, TextNode, Variable,
token_kwargs,
Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs,
)
from .library import Library
register = Library()

View File

@ -35,9 +35,6 @@ def update_installed_apps(**kwargs):
# Rebuild management commands cache
from django.core.management import get_commands
get_commands.cache_clear()
# Rebuild templatetags module cache.
from django.template.base import get_templatetags_modules
get_templatetags_modules.cache_clear()
# Rebuild get_app_template_dirs cache.
from django.template.utils import get_app_template_dirs
get_app_template_dirs.cache_clear()

View File

@ -13,9 +13,11 @@ available to your templates using the :ttag:`{% load %}<load>` tag.
Code layout
-----------
Custom template tags and filters must live inside a Django app. If they relate
to an existing app it makes sense to bundle them there; otherwise, you should
create a new app to hold them.
The most common place to specify custom template tags and filters is inside
a Django app. If they relate to an existing app, it makes sense to bundle them
there; otherwise, they can be added to a new app. When a Django app is added
to :setting:`INSTALLED_APPS`, any tags it defines in the conventional location
described below are automatically made available to load within templates.
The app should contain a ``templatetags`` directory, at the same level as
``models.py``, ``views.py``, etc. If this doesn't already exist, create it -
@ -63,6 +65,15 @@ following::
register = template.Library()
.. versionadded:: 1.9
Alternatively, template tag modules can be registered through the
``'libraries'`` argument to
:class:`~django.template.backends.django.DjangoTemplates`. This is useful if
you want to use a different label from the template tag module name when
loading template tags. It also enables you to register tags without installing
an application.
.. admonition:: Behind the scenes
For a ton of examples, read the source code for Django's default filters

View File

@ -41,7 +41,7 @@ lower level APIs:
Configuring an engine
=====================
.. class:: Engine([dirs][, app_dirs][, allowed_include_roots][, context_processors][, debug][, loaders][, string_if_invalid][, file_charset])
.. class:: Engine([dirs][, app_dirs][, allowed_include_roots][, context_processors][, debug][, loaders][, string_if_invalid][, file_charset][, libraries][, builtins])
.. versionadded:: 1.8
@ -114,6 +114,34 @@ Configuring an engine
It defaults to ``'utf-8'``.
* ``'libraries'``: A dictionary of labels and dotted Python paths of template
tag modules to register with the template engine. This is used to add new
libraries or provide alternate labels for existing ones. For example::
Engine(
libraries={
'myapp_tags': 'path.to.myapp.tags',
'admin.urls': 'django.contrib.admin.templatetags.admin_urls',
},
)
Libraries can be loaded by passing the corresponding dictionary key to
the :ttag:`{% load %}<load>` tag.
* ``'builtins'``: A list of dotted Python paths of template tag modules to
add to :doc:`built-ins </ref/templates/builtins>`. For example::
Engine(
builtins=['myapp.builtins'],
)
Tags and filters from built-in libraries can be used without first calling
the :ttag:`{% load %}<load>` tag.
.. versionadded:: 1.9
The ``libraries`` and ``builtins`` arguments were added.
.. staticmethod:: Engine.get_default()
When a Django project configures one and only one

View File

@ -263,6 +263,10 @@ Templates
* :ref:`Debug page integration <template-debug-integration>` for custom
template engines was added.
* The :class:`~django.template.backends.django.DjangoTemplates` backend gained
the ability to register libraries and builtins explicitly through the
template :setting:`OPTIONS <TEMPLATES-OPTIONS>`.
Requests and Responses
^^^^^^^^^^^^^^^^^^^^^^
@ -467,6 +471,28 @@ You don't need any of this if you're querying the database through the ORM,
even if you're using :meth:`raw() <django.db.models.query.QuerySet.raw>`
queries. The ORM takes care of managing time zone information.
Template tag modules are imported when templates are configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The :class:`~django.template.backends.django.DjangoTemplates` backend now
performs discovery on installed template tag modules when instantiated. This
update enables libraries to be provided explicitly via the ``'libraries'``
key of :setting:`OPTIONS <TEMPLATES-OPTIONS>` when defining a
:class:`~django.template.backends.django.DjangoTemplates` backend. Import
or syntax errors in template tag modules now fail early at instantiation time
rather than when a template with a :ttag:`{% load %}<load>` tag is first
compiled.
``django.template.base.add_to_builtins()`` is removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Although it was a private API, projects commonly used ``add_to_builtins()`` to
make template tags and filters available without using the
:ttag:`{% load %}<load>` tag. This API has been formalized. Projects should now
define built-in libraries via the ``'builtins'`` key of :setting:`OPTIONS
<TEMPLATES-OPTIONS>` when defining a
:class:`~django.template.backends.django.DjangoTemplates` backend.
Miscellaneous
~~~~~~~~~~~~~

View File

@ -401,6 +401,34 @@ applications. This generic name was kept for backwards-compatibility.
It defaults to the value of :setting:`FILE_CHARSET`.
* ``'libraries'``: A dictionary of labels and dotted Python paths of template
tag modules to register with the template engine. This can be used to add
new libraries or provide alternate labels for existing ones. For example::
OPTIONS={
'libraries': {
'myapp_tags': 'path.to.myapp.tags',
'admin.urls': 'django.contrib.admin.templatetags.admin_urls',
},
}
Libraries can be loaded by passing the corresponding dictionary key to
the :ttag:`{% load %}<load>` tag.
* ``'builtins'``: A list of dotted Python paths of template tag modules to
add to :doc:`built-ins </ref/templates/builtins>`. For example::
OPTIONS={
'builtins': ['myapp.builtins'],
}
Tags and filters from built-in libraries can be used without first calling
the :ttag:`{% load %} <load>` tag.
.. versionadded:: 1.9
The ``libraries`` and ``builtins`` arguments were added.
.. module:: django.template.backends.jinja2
.. class:: Jinja2

View File

View File

@ -0,0 +1,3 @@
from django.template import Library
register = Library()

View File

@ -0,0 +1,3 @@
from django.template import Library
register = Library()

View File

@ -0,0 +1,3 @@
from django.template import Library
register = Library()

View File

@ -0,0 +1 @@
import DoesNotExist # noqa

View File

@ -2,7 +2,8 @@ from template_tests.test_response import test_processor_name
from django.template import RequestContext
from django.template.backends.django import DjangoTemplates
from django.test import RequestFactory, ignore_warnings
from django.template.library import InvalidTemplateLibrary
from django.test import RequestFactory, ignore_warnings, override_settings
from django.utils.deprecation import RemovedInDjango20Warning
from .test_dummy import TemplateStringsTests
@ -51,3 +52,78 @@ class DjangoTemplatesTests(TemplateStringsTests):
"the two arguments refer to the same request.")
with self.assertRaisesMessage(ValueError, msg):
template.render(request_context, other_request)
@override_settings(INSTALLED_APPS=['template_backends.apps.good'])
def test_templatetag_discovery(self):
engine = DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {
'libraries': {
'alternate': 'template_backends.apps.good.templatetags.good_tags',
'override': 'template_backends.apps.good.templatetags.good_tags',
},
},
})
# libraries are discovered from installed applications
self.assertEqual(
engine.engine.libraries['good_tags'],
'template_backends.apps.good.templatetags.good_tags',
)
self.assertEqual(
engine.engine.libraries['subpackage.tags'],
'template_backends.apps.good.templatetags.subpackage.tags',
)
# libraries are discovered from django.templatetags
self.assertEqual(
engine.engine.libraries['static'],
'django.templatetags.static',
)
# libraries passed in OPTIONS are registered
self.assertEqual(
engine.engine.libraries['alternate'],
'template_backends.apps.good.templatetags.good_tags',
)
# libraries passed in OPTIONS take precedence over discovered ones
self.assertEqual(
engine.engine.libraries['override'],
'template_backends.apps.good.templatetags.good_tags',
)
@override_settings(INSTALLED_APPS=['template_backends.apps.importerror'])
def test_templatetag_discovery_import_error(self):
"""
Import errors in tag modules should be reraised with a helpful message.
"""
with self.assertRaisesMessage(
InvalidTemplateLibrary,
"ImportError raised when trying to load "
"'template_backends.apps.importerror.templatetags.broken_tags'"
):
DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {},
})
def test_builtins_discovery(self):
engine = DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {
'builtins': ['template_backends.apps.good.templatetags.good_tags'],
},
})
self.assertEqual(
engine.engine.builtins, [
'django.template.defaulttags',
'django.template.defaultfilters',
'django.template.loader_tags',
'template_backends.apps.good.templatetags.good_tags',
]
)

View File

@ -6,6 +6,10 @@ from ..utils import setup
class CacheTagTests(SimpleTestCase):
libraries = {
'cache': 'django.templatetags.cache',
'custom': 'template_tests.templatetags.custom',
}
def tearDown(self):
cache.clear()
@ -121,7 +125,7 @@ class CacheTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.engine = Engine()
cls.engine = Engine(libraries={'cache': 'django.templatetags.cache'})
super(CacheTests, cls).setUpClass()
def test_cache_regression_20130(self):

View File

@ -6,6 +6,7 @@ from ..utils import setup
class CycleTagTests(SimpleTestCase):
libraries = {'future': 'django.templatetags.future'}
@setup({'cycle01': '{% cycle a %}'})
def test_cycle01(self):

View File

@ -56,6 +56,7 @@ inheritance_templates = {
class InheritanceTests(SimpleTestCase):
libraries = {'testtags': 'template_tests.templatetags.testtags'}
@setup(inheritance_templates)
def test_inheritance01(self):

View File

@ -6,6 +6,7 @@ from ..utils import setup
class FirstOfTagTests(SimpleTestCase):
libraries = {'future': 'django.templatetags.future'}
@setup({'firstof01': '{% firstof a b c %}'})
def test_firstof01(self):

View File

@ -6,6 +6,7 @@ from ..utils import setup
class ForTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'for-tag01': '{% for val in values %}{{ val }}{% endfor %}'})
def test_for_tag01(self):

View File

@ -10,6 +10,10 @@ from ..utils import setup
class I18nTagTests(SimpleTestCase):
libraries = {
'custom': 'template_tests.templatetags.custom',
'i18n': 'django.templatetags.i18n',
}
@setup({'i18n01': '{% load i18n %}{% trans \'xxxyyyxxx\' %}'})
def test_i18n01(self):

View File

@ -5,6 +5,7 @@ from ..utils import setup
class IfChangedTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'ifchanged01': '{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}'})
def test_ifchanged01(self):

View File

@ -13,6 +13,7 @@ include_fail_templates = {
class IncludeTagTests(SimpleTestCase):
libraries = {'bad_tag': 'template_tests.templatetags.bad_tag'}
@setup({'include01': '{% include "basic-syntax01" %}'}, basic_templates)
def test_include01(self):

View File

@ -4,6 +4,7 @@ from ..utils import setup
class InvalidStringTests(SimpleTestCase):
libraries = {'i18n': 'django.templatetags.i18n'}
@setup({'invalidstr01': '{{ var|default:"Foo" }}'})
def test_invalidstr01(self):

View File

@ -5,6 +5,10 @@ from ..utils import setup
class LoadTagTests(SimpleTestCase):
libraries = {
'subpackage.echo': 'template_tests.templatetags.subpackage.echo',
'testtags': 'template_tests.templatetags.testtags',
}
@setup({'load01': '{% load testtags subpackage.echo %}{% echo test %} {% echo2 "test" %}'})
def test_load01(self):
@ -42,30 +46,30 @@ class LoadTagTests(SimpleTestCase):
# {% load %} tag errors
@setup({'load07': '{% load echo other_echo bad_tag from testtags %}'})
def test_load07(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'bad_tag' is not a valid tag or filter in tag library 'testtags'"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load07')
@setup({'load08': '{% load echo other_echo bad_tag from %}'})
def test_load08(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'echo' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load08')
@setup({'load09': '{% load from testtags %}'})
def test_load09(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'from' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load09')
@setup({'load10': '{% load echo from bad_library %}'})
def test_load10(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'bad_library' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load10')
@setup({'load11': '{% load subpackage.echo_invalid %}'})
def test_load11(self):
with self.assertRaises(TemplateSyntaxError):
self.engine.get_template('load11')
@setup({'load12': '{% load subpackage.missing %}'})
def test_load12(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'subpackage.missing' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load12')

View File

@ -5,6 +5,7 @@ from ..utils import setup
class SimpleTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'simpletag-renamed01': '{% load custom %}{% minusone 7 %}'})
def test_simpletag_renamed01(self):

View File

@ -7,6 +7,7 @@ from ..utils import setup
@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/")
class StaticTagTests(SimpleTestCase):
libraries = {'static': 'django.templatetags.static'}
@setup({'static-prefixtag01': '{% load static %}{% get_static_prefix %}'})
def test_static_prefixtag01(self):

View File

@ -6,6 +6,7 @@ from ..utils import setup
class WidthRatioTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'widthratio01': '{% widthratio a b 0 %}'})
def test_widthratio01(self):

View File

@ -1 +0,0 @@
import nonexistent.module # NOQA

View File

@ -0,0 +1,22 @@
from django.template import Library, Node
register = Library()
class EchoNode(Node):
def __init__(self, contents):
self.contents = contents
def render(self, context):
return ' '.join(self.contents)
@register.tag
def echo(parser, token):
return EchoNode(token.contents.split()[1:])
register.tag('other_echo', echo)
@register.filter
def upper(value):
return value.upper()

View File

@ -4,18 +4,26 @@ import os
from django.template import Context, Engine, TemplateSyntaxError
from django.template.base import Node
from django.template.library import InvalidTemplateLibrary
from django.test import SimpleTestCase, ignore_warnings
from django.test.utils import extend_sys_path
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from .templatetags import custom, inclusion
from .utils import ROOT
LIBRARIES = {
'custom': 'template_tests.templatetags.custom',
'inclusion': 'template_tests.templatetags.inclusion',
}
class CustomFilterTests(SimpleTestCase):
def test_filter(self):
t = Engine().from_string("{% load custom %}{{ string|trim:5 }}")
engine = Engine(libraries=LIBRARIES)
t = engine.from_string("{% load custom %}{{ string|trim:5 }}")
self.assertEqual(
t.render(Context({"string": "abcdefghijklmnopqrstuvwxyz"})),
"abcde"
@ -26,7 +34,7 @@ class TagTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.engine = Engine(app_dirs=True)
cls.engine = Engine(app_dirs=True, libraries=LIBRARIES)
super(TagTestCase, cls).setUpClass()
def verify_tag(self, tag, name):
@ -269,7 +277,7 @@ class InclusionTagTests(TagTestCase):
"""
#23441 -- InclusionNode shouldn't modify its nodelist at render time.
"""
engine = Engine(app_dirs=True)
engine = Engine(app_dirs=True, libraries=LIBRARIES)
template = engine.from_string('{% load inclusion %}{% inclusion_no_params %}')
count = template.nodelist.get_nodes_by_type(Node)
template.render(Context({}))
@ -281,7 +289,7 @@ class InclusionTagTests(TagTestCase):
when rendering. Otherwise, leftover values such as blocks from
extending can interfere with subsequent rendering.
"""
engine = Engine(app_dirs=True)
engine = Engine(app_dirs=True, libraries=LIBRARIES)
template = engine.from_string('{% load inclusion %}{% inclusion_extends1 %}{% inclusion_extends2 %}')
self.assertEqual(template.render(Context({})).strip(), 'one\ntwo')
@ -313,34 +321,37 @@ class TemplateTagLoadingTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.egg_dir = os.path.join(ROOT, 'eggs')
cls.engine = Engine()
super(TemplateTagLoadingTests, cls).setUpClass()
def test_load_error(self):
ttext = "{% load broken_tag %}"
with self.assertRaises(TemplateSyntaxError) as e:
self.engine.from_string(ttext)
self.assertIn('ImportError', e.exception.args[0])
self.assertIn('Xtemplate', e.exception.args[0])
msg = (
"Invalid template library specified. ImportError raised when "
"trying to load 'template_tests.broken_tag': cannot import name "
"'?Xtemplate'?"
)
with six.assertRaisesRegex(self, InvalidTemplateLibrary, msg):
Engine(libraries={
'broken_tag': 'template_tests.broken_tag',
})
def test_load_error_egg(self):
ttext = "{% load broken_egg %}"
egg_name = '%s/tagsegg.egg' % self.egg_dir
msg = (
"Invalid template library specified. ImportError raised when "
"trying to load 'tagsegg.templatetags.broken_egg': cannot "
"import name '?Xtemplate'?"
)
with extend_sys_path(egg_name):
with self.assertRaises(TemplateSyntaxError):
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
try:
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
except TemplateSyntaxError as e:
self.assertIn('ImportError', e.args[0])
self.assertIn('Xtemplate', e.args[0])
with six.assertRaisesRegex(self, InvalidTemplateLibrary, msg):
Engine(libraries={
'broken_egg': 'tagsegg.templatetags.broken_egg',
})
def test_load_working_egg(self):
ttext = "{% load working_egg %}"
egg_name = '%s/tagsegg.egg' % self.egg_dir
with extend_sys_path(egg_name):
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
engine = Engine(libraries={
'working_egg': 'tagsegg.templatetags.working_egg',
})
engine.from_string(ttext)

View File

@ -14,7 +14,10 @@ OTHER_DIR = os.path.join(ROOT, 'other_templates')
class DeprecatedRenderToStringTest(SimpleTestCase):
def setUp(self):
self.engine = Engine(dirs=[TEMPLATE_DIR])
self.engine = Engine(
dirs=[TEMPLATE_DIR],
libraries={'custom': 'template_tests.templatetags.custom'},
)
def test_basic_context(self):
self.assertEqual(

View File

@ -0,0 +1,132 @@
from django.template import Library
from django.template.base import Node
from django.test import TestCase
class FilterRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_filter(self):
@self.library.filter
def func():
return ''
self.assertEqual(self.library.filters['func'], func)
def test_filter_parens(self):
@self.library.filter()
def func():
return ''
self.assertEqual(self.library.filters['func'], func)
def test_filter_name_arg(self):
@self.library.filter('name')
def func():
return ''
self.assertEqual(self.library.filters['name'], func)
def test_filter_name_kwarg(self):
@self.library.filter(name='name')
def func():
return ''
self.assertEqual(self.library.filters['name'], func)
def test_filter_call(self):
def func():
return ''
self.library.filter('name', func)
self.assertEqual(self.library.filters['name'], func)
def test_filter_invalid(self):
msg = "Unsupported arguments to Library.filter: (None, '')"
with self.assertRaisesMessage(ValueError, msg):
self.library.filter(None, '')
class InclusionTagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_inclusion_tag(self):
@self.library.inclusion_tag('template.html')
def func():
return ''
self.assertIn('func', self.library.tags)
def test_inclusion_tag_name(self):
@self.library.inclusion_tag('template.html', name='name')
def func():
return ''
self.assertIn('name', self.library.tags)
class SimpleTagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_simple_tag(self):
@self.library.simple_tag
def func():
return ''
self.assertIn('func', self.library.tags)
def test_simple_tag_parens(self):
@self.library.simple_tag()
def func():
return ''
self.assertIn('func', self.library.tags)
def test_simple_tag_name_kwarg(self):
@self.library.simple_tag(name='name')
def func():
return ''
self.assertIn('name', self.library.tags)
def test_simple_tag_invalid(self):
msg = "Invalid arguments provided to simple_tag"
with self.assertRaisesMessage(ValueError, msg):
self.library.simple_tag('invalid')
class TagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_tag(self):
@self.library.tag
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['func'], func)
def test_tag_parens(self):
@self.library.tag()
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['func'], func)
def test_tag_name_arg(self):
@self.library.tag('name')
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['name'], func)
def test_tag_name_kwarg(self):
@self.library.tag(name='name')
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['name'], func)
def test_tag_call(self):
def func(parser, token):
return Node()
self.library.tag('name', func)
self.assertEqual(self.library.tags['name'], func)
def test_tag_invalid(self):
msg = "Unsupported arguments to Library.tag: (None, '')"
with self.assertRaisesMessage(ValueError, msg):
self.library.tag(None, '')

View File

@ -49,7 +49,7 @@ class ErrorIndexTest(TestCase):
'range': range(5),
'five': 5,
})
engine = Engine(debug=True)
engine = Engine(debug=True, libraries={'bad_tag': 'template_tests.templatetags.bad_tag'})
for source, expected_error_source_index in tests:
template = engine.from_string(source)
try:

View File

@ -9,6 +9,7 @@ from django.template import Library, TemplateSyntaxError
from django.template.base import (
TOKEN_BLOCK, FilterExpression, Parser, Token, Variable,
)
from django.template.defaultfilters import register as filter_library
from django.utils import six
@ -24,7 +25,7 @@ class ParserTests(TestCase):
def test_filter_parsing(self):
c = {"article": {"section": "News"}}
p = Parser("")
p = Parser("", builtins=[filter_library])
def fe_test(s, val):
self.assertEqual(FilterExpression(s, p).resolve(c), val)

View File

@ -97,7 +97,10 @@ class TemplateTests(SimpleTestCase):
Errors raised while compiling nodes should include the token
information.
"""
engine = Engine(debug=True)
engine = Engine(
debug=True,
libraries={'bad_tag': 'template_tests.templatetags.bad_tag'},
)
with self.assertRaises(RuntimeError) as e:
engine.from_string("{% load bad_tag %}{% badtag %}")
self.assertEqual(e.exception.template_debug['during'], '{% badtag %}')

View File

@ -5,9 +5,6 @@ from __future__ import unicode_literals
import functools
import os
from django import template
from django.template import Library
from django.template.base import libraries
from django.template.engine import Engine
from django.test.utils import override_settings
from django.utils._os import upath
@ -49,14 +46,17 @@ def setup(templates, *args, **kwargs):
]
def decorator(func):
@register_test_tags
# Make Engine.get_default() raise an exception to ensure that tests
# are properly isolated from Django's global settings.
@override_settings(TEMPLATES=None)
@functools.wraps(func)
def inner(self):
# Set up custom template tag libraries if specified
libraries = getattr(self, 'libraries', {})
self.engine = Engine(
allowed_include_roots=[ROOT],
libraries=libraries,
loaders=loaders,
)
func(self)
@ -66,6 +66,7 @@ def setup(templates, *args, **kwargs):
self.engine = Engine(
allowed_include_roots=[ROOT],
libraries=libraries,
loaders=loaders,
string_if_invalid='INVALID',
)
@ -75,6 +76,7 @@ def setup(templates, *args, **kwargs):
self.engine = Engine(
allowed_include_roots=[ROOT],
debug=True,
libraries=libraries,
loaders=loaders,
)
func(self)
@ -85,43 +87,9 @@ def setup(templates, *args, **kwargs):
return decorator
# Custom template tag for tests
register = Library()
class EchoNode(template.Node):
def __init__(self, contents):
self.contents = contents
def render(self, context):
return ' '.join(self.contents)
@register.tag
def echo(parser, token):
return EchoNode(token.contents.split()[1:])
register.tag('other_echo', echo)
@register.filter
def upper(value):
return value.upper()
def register_test_tags(func):
@functools.wraps(func)
def inner(self):
libraries['testtags'] = register
try:
func(self)
finally:
del libraries['testtags']
return inner
# Helper objects
class SomeException(Exception):
silent_variable_failure = True