summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPreston Timmons <prestontimmons@gmail.com>2015-05-08 15:10:36 -0500
committerPreston Timmons <prestontimmons@gmail.com>2015-05-21 09:12:06 -0500
commit655f52491505932ef04264de2bce21a03f3a7cd0 (patch)
treec38eca24b887466fc2385fc773ff34247567c732
parent7b8008a078ffdfd18ebbe78fecbb92cdddf2f304 (diff)
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.
-rw-r--r--django/contrib/admindocs/views.py122
-rw-r--r--django/template/__init__.py2
-rw-r--r--django/template/backends/django.py60
-rw-r--r--django/template/base.py411
-rw-r--r--django/template/defaultfilters.py3
-rw-r--r--django/template/defaulttags.py80
-rw-r--r--django/template/engine.py25
-rw-r--r--django/template/library.py327
-rw-r--r--django/template/loader_tags.py4
-rw-r--r--django/test/signals.py3
-rw-r--r--docs/howto/custom-template-tags.txt17
-rw-r--r--docs/ref/templates/api.txt30
-rw-r--r--docs/releases/1.9.txt26
-rw-r--r--docs/topics/templates.txt28
-rw-r--r--tests/template_backends/apps/__init__.py0
-rw-r--r--tests/template_backends/apps/good/__init__.py0
-rw-r--r--tests/template_backends/apps/good/templatetags/__init__.py0
-rw-r--r--tests/template_backends/apps/good/templatetags/empty.py0
-rw-r--r--tests/template_backends/apps/good/templatetags/good_tags.py3
-rw-r--r--tests/template_backends/apps/good/templatetags/override.py3
-rw-r--r--tests/template_backends/apps/good/templatetags/subpackage/__init__.py0
-rw-r--r--tests/template_backends/apps/good/templatetags/subpackage/tags.py3
-rw-r--r--tests/template_backends/apps/importerror/__init__.py0
-rw-r--r--tests/template_backends/apps/importerror/templatetags/__init__.py0
-rw-r--r--tests/template_backends/apps/importerror/templatetags/broken_tags.py1
-rw-r--r--tests/template_backends/test_django.py78
-rw-r--r--tests/template_tests/broken_tag.py (renamed from tests/template_tests/templatetags/broken_tag.py)0
-rw-r--r--tests/template_tests/syntax_tests/test_cache.py6
-rw-r--r--tests/template_tests/syntax_tests/test_cycle.py1
-rw-r--r--tests/template_tests/syntax_tests/test_extends.py1
-rw-r--r--tests/template_tests/syntax_tests/test_firstof.py1
-rw-r--r--tests/template_tests/syntax_tests/test_for.py1
-rw-r--r--tests/template_tests/syntax_tests/test_i18n.py4
-rw-r--r--tests/template_tests/syntax_tests/test_if_changed.py1
-rw-r--r--tests/template_tests/syntax_tests/test_include.py1
-rw-r--r--tests/template_tests/syntax_tests/test_invalid_string.py1
-rw-r--r--tests/template_tests/syntax_tests/test_load.py24
-rw-r--r--tests/template_tests/syntax_tests/test_simple_tag.py1
-rw-r--r--tests/template_tests/syntax_tests/test_static.py1
-rw-r--r--tests/template_tests/syntax_tests/test_width_ratio.py1
-rw-r--r--tests/template_tests/templatetags/subpackage/echo_invalid.py1
-rw-r--r--tests/template_tests/templatetags/testtags.py22
-rw-r--r--tests/template_tests/test_custom.py57
-rw-r--r--tests/template_tests/test_engine.py5
-rw-r--r--tests/template_tests/test_library.py132
-rw-r--r--tests/template_tests/test_nodelist.py2
-rw-r--r--tests/template_tests/test_parser.py3
-rw-r--r--tests/template_tests/tests.py5
-rw-r--r--tests/template_tests/utils.py46
49 files changed, 949 insertions, 594 deletions
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index dc7f384393..35aad4c897 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -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,31 +55,32 @@ 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]
- 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__)
- if title:
- title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
- if body:
- 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:
+ 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__)
+ if title:
+ title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
+ if body:
+ body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
+ for key in metadata:
+ metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
tag_library = module_name.split('.')[-1]
- tags.append({
- 'name': tag_name,
- 'title': title,
- 'body': body,
- 'meta': metadata,
- 'library': tag_library,
- })
+ tags.append({
+ 'name': tag_name,
+ 'title': title,
+ 'body': body,
+ 'meta': metadata,
+ 'library': tag_library,
+ })
kwargs.update({'tags': tags})
return super(TemplateTagIndexView, self).get_context_data(**kwargs)
@@ -93,31 +89,32 @@ 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]
- 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__)
- if title:
- title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
- if body:
- 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:
+ 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__)
+ if title:
+ title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
+ if body:
+ body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
+ for key in metadata:
+ metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
tag_library = module_name.split('.')[-1]
- filters.append({
- 'name': filter_name,
- 'title': title,
- 'body': body,
- 'meta': metadata,
- 'library': tag_library,
- })
+ filters.append({
+ 'name': filter_name,
+ 'title': title,
+ 'body': body,
+ 'meta': metadata,
+ 'library': tag_library,
+ })
kwargs.update({'filters': filters})
return super(TemplateFilterIndexView, self).get_context_data(**kwargs)
@@ -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"""
diff --git a/django/template/__init__.py b/django/template/__init__.py
index cd1bf167c8..20f4986837 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -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')
diff --git a/django/template/backends/django.py b/django/template/backends/django.py
index 9c2d24af13..0f5481d941 100644
--- a/django/template/backends/django.py
+++ b/django/template/backends/django.py
@@ -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]
diff --git a/django/template/base.py b/django/template/base.py
index 1b05fe57b8..deebde7a7c 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -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')
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 7073bfaf4f..0ab957f056 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -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()
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 54d0d5d449..fd64b49d13 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -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))
- 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)
+ # 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:
- for taglib in bits[1:]:
- # add the library to the parser
- try:
- lib = get_library(taglib)
- parser.add_library(lib)
- except InvalidTemplateLibrary as e:
- raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
- (taglib, e))
+ # 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)
return LoadNode()
diff --git a/django/template/engine.py b/django/template/engine.py
index 241759229b..e7cd522527 100644
--- a/django/template/engine.py
+++ b/django/template/engine.py
@@ -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)
diff --git a/django/template/library.py b/django/template/library.py
new file mode 100644
index 0000000000..182dacf964
--- /dev/null
+++ b/django/template/library.py
@@ -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,
+ )
diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py
index 4c662168e9..de77ecbb7a 100644
--- a/django/template/loader_tags.py
+++ b/django/template/loader_tags.py
@@ -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()
diff --git a/django/test/signals.py b/django/test/signals.py
index a7774adb08..6d8a914ad7 100644
--- a/django/test/signals.py
+++ b/django/test/signals.py
@@ -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()
diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt
index ec675aada2..c4e414cdb5 100644
--- a/docs/howto/custom-template-tags.txt
+++ b/docs/howto/custom-template-tags.txt
@@ -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
diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt
index 4a9a73f758..6ff2dd6c7b 100644
--- a/docs/ref/templates/api.txt
+++ b/docs/ref/templates/api.txt
@@ -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
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 76e8085fbd..1aff56aef3 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -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
~~~~~~~~~~~~~
diff --git a/docs/topics/templates.txt b/docs/topics/templates.txt
index 3810ea7483..66bb8314d6 100644
--- a/docs/topics/templates.txt
+++ b/docs/topics/templates.txt
@@ -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
diff --git a/tests/template_backends/apps/__init__.py b/tests/template_backends/apps/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/__init__.py
diff --git a/tests/template_backends/apps/good/__init__.py b/tests/template_backends/apps/good/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/good/__init__.py
diff --git a/tests/template_backends/apps/good/templatetags/__init__.py b/tests/template_backends/apps/good/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/__init__.py
diff --git a/tests/template_backends/apps/good/templatetags/empty.py b/tests/template_backends/apps/good/templatetags/empty.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/empty.py
diff --git a/tests/template_backends/apps/good/templatetags/good_tags.py b/tests/template_backends/apps/good/templatetags/good_tags.py
new file mode 100644
index 0000000000..9bec93d8e5
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/good_tags.py
@@ -0,0 +1,3 @@
+from django.template import Library
+
+register = Library()
diff --git a/tests/template_backends/apps/good/templatetags/override.py b/tests/template_backends/apps/good/templatetags/override.py
new file mode 100644
index 0000000000..9bec93d8e5
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/override.py
@@ -0,0 +1,3 @@
+from django.template import Library
+
+register = Library()
diff --git a/tests/template_backends/apps/good/templatetags/subpackage/__init__.py b/tests/template_backends/apps/good/templatetags/subpackage/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/subpackage/__init__.py
diff --git a/tests/template_backends/apps/good/templatetags/subpackage/tags.py b/tests/template_backends/apps/good/templatetags/subpackage/tags.py
new file mode 100644
index 0000000000..9bec93d8e5
--- /dev/null
+++ b/tests/template_backends/apps/good/templatetags/subpackage/tags.py
@@ -0,0 +1,3 @@
+from django.template import Library
+
+register = Library()
diff --git a/tests/template_backends/apps/importerror/__init__.py b/tests/template_backends/apps/importerror/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/importerror/__init__.py
diff --git a/tests/template_backends/apps/importerror/templatetags/__init__.py b/tests/template_backends/apps/importerror/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/template_backends/apps/importerror/templatetags/__init__.py
diff --git a/tests/template_backends/apps/importerror/templatetags/broken_tags.py b/tests/template_backends/apps/importerror/templatetags/broken_tags.py
new file mode 100644
index 0000000000..4f240e5923
--- /dev/null
+++ b/tests/template_backends/apps/importerror/templatetags/broken_tags.py
@@ -0,0 +1 @@
+import DoesNotExist # noqa
diff --git a/tests/template_backends/test_django.py b/tests/template_backends/test_django.py
index a8cc0d58b8..2a21b04b5a 100644
--- a/tests/template_backends/test_django.py
+++ b/tests/template_backends/test_django.py
@@ -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',
+ ]
+ )
diff --git a/tests/template_tests/templatetags/broken_tag.py b/tests/template_tests/broken_tag.py
index 414dc8412f..414dc8412f 100644
--- a/tests/template_tests/templatetags/broken_tag.py
+++ b/tests/template_tests/broken_tag.py
diff --git a/tests/template_tests/syntax_tests/test_cache.py b/tests/template_tests/syntax_tests/test_cache.py
index f088510d5d..2039e7808c 100644
--- a/tests/template_tests/syntax_tests/test_cache.py
+++ b/tests/template_tests/syntax_tests/test_cache.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_cycle.py b/tests/template_tests/syntax_tests/test_cycle.py
index e2edf94c8d..c478faff52 100644
--- a/tests/template_tests/syntax_tests/test_cycle.py
+++ b/tests/template_tests/syntax_tests/test_cycle.py
@@ -6,6 +6,7 @@ from ..utils import setup
class CycleTagTests(SimpleTestCase):
+ libraries = {'future': 'django.templatetags.future'}
@setup({'cycle01': '{% cycle a %}'})
def test_cycle01(self):
diff --git a/tests/template_tests/syntax_tests/test_extends.py b/tests/template_tests/syntax_tests/test_extends.py
index 1b0ff36a3c..3e65f4bf90 100644
--- a/tests/template_tests/syntax_tests/test_extends.py
+++ b/tests/template_tests/syntax_tests/test_extends.py
@@ -56,6 +56,7 @@ inheritance_templates = {
class InheritanceTests(SimpleTestCase):
+ libraries = {'testtags': 'template_tests.templatetags.testtags'}
@setup(inheritance_templates)
def test_inheritance01(self):
diff --git a/tests/template_tests/syntax_tests/test_firstof.py b/tests/template_tests/syntax_tests/test_firstof.py
index a3c8c83151..79eb381c69 100644
--- a/tests/template_tests/syntax_tests/test_firstof.py
+++ b/tests/template_tests/syntax_tests/test_firstof.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_for.py b/tests/template_tests/syntax_tests/test_for.py
index 566b86f3d8..8045ea4990 100644
--- a/tests/template_tests/syntax_tests/test_for.py
+++ b/tests/template_tests/syntax_tests/test_for.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_i18n.py b/tests/template_tests/syntax_tests/test_i18n.py
index da53a8c279..1d0f83478a 100644
--- a/tests/template_tests/syntax_tests/test_i18n.py
+++ b/tests/template_tests/syntax_tests/test_i18n.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_if_changed.py b/tests/template_tests/syntax_tests/test_if_changed.py
index 62f9563020..09cdc34a88 100644
--- a/tests/template_tests/syntax_tests/test_if_changed.py
+++ b/tests/template_tests/syntax_tests/test_if_changed.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_include.py b/tests/template_tests/syntax_tests/test_include.py
index 5a05b32c33..fc1554a17e 100644
--- a/tests/template_tests/syntax_tests/test_include.py
+++ b/tests/template_tests/syntax_tests/test_include.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_invalid_string.py b/tests/template_tests/syntax_tests/test_invalid_string.py
index 2bb8c71fd1..18520aa7bf 100644
--- a/tests/template_tests/syntax_tests/test_invalid_string.py
+++ b/tests/template_tests/syntax_tests/test_invalid_string.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_load.py b/tests/template_tests/syntax_tests/test_load.py
index 711a9da632..919bbf1346 100644
--- a/tests/template_tests/syntax_tests/test_load.py
+++ b/tests/template_tests/syntax_tests/test_load.py
@@ -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')
diff --git a/tests/template_tests/syntax_tests/test_simple_tag.py b/tests/template_tests/syntax_tests/test_simple_tag.py
index b89af4c72b..5e6ce4733e 100644
--- a/tests/template_tests/syntax_tests/test_simple_tag.py
+++ b/tests/template_tests/syntax_tests/test_simple_tag.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_static.py b/tests/template_tests/syntax_tests/test_static.py
index b646ac2bb8..8ff90f5a5c 100644
--- a/tests/template_tests/syntax_tests/test_static.py
+++ b/tests/template_tests/syntax_tests/test_static.py
@@ -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):
diff --git a/tests/template_tests/syntax_tests/test_width_ratio.py b/tests/template_tests/syntax_tests/test_width_ratio.py
index 9799b7d6ab..8206b83c58 100644
--- a/tests/template_tests/syntax_tests/test_width_ratio.py
+++ b/tests/template_tests/syntax_tests/test_width_ratio.py
@@ -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):
diff --git a/tests/template_tests/templatetags/subpackage/echo_invalid.py b/tests/template_tests/templatetags/subpackage/echo_invalid.py
deleted file mode 100644
index 0764b9c8fa..0000000000
--- a/tests/template_tests/templatetags/subpackage/echo_invalid.py
+++ /dev/null
@@ -1 +0,0 @@
-import nonexistent.module # NOQA
diff --git a/tests/template_tests/templatetags/testtags.py b/tests/template_tests/templatetags/testtags.py
new file mode 100644
index 0000000000..41b65a2ed1
--- /dev/null
+++ b/tests/template_tests/templatetags/testtags.py
@@ -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()
diff --git a/tests/template_tests/test_custom.py b/tests/template_tests/test_custom.py
index b5c125c61c..11c04f870c 100644
--- a/tests/template_tests/test_custom.py
+++ b/tests/template_tests/test_custom.py
@@ -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)
diff --git a/tests/template_tests/test_engine.py b/tests/template_tests/test_engine.py
index d9929660cc..a72a430e3e 100644
--- a/tests/template_tests/test_engine.py
+++ b/tests/template_tests/test_engine.py
@@ -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(
diff --git a/tests/template_tests/test_library.py b/tests/template_tests/test_library.py
new file mode 100644
index 0000000000..50a00ca082
--- /dev/null
+++ b/tests/template_tests/test_library.py
@@ -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, '')
diff --git a/tests/template_tests/test_nodelist.py b/tests/template_tests/test_nodelist.py
index 3f28aff92c..3070c9d68a 100644
--- a/tests/template_tests/test_nodelist.py
+++ b/tests/template_tests/test_nodelist.py
@@ -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:
diff --git a/tests/template_tests/test_parser.py b/tests/template_tests/test_parser.py
index dd22c83836..6ff57c2cf5 100644
--- a/tests/template_tests/test_parser.py
+++ b/tests/template_tests/test_parser.py
@@ -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)
diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py
index 436502305e..39f0b427d6 100644
--- a/tests/template_tests/tests.py
+++ b/tests/template_tests/tests.py
@@ -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 %}')
diff --git a/tests/template_tests/utils.py b/tests/template_tests/utils.py
index 36f4d95678..d78f0eac6c 100644
--- a/tests/template_tests/utils.py
+++ b/tests/template_tests/utils.py
@@ -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