Browse Source

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.
Preston Timmons 10 years ago
parent
commit
655f524915
49 changed files with 949 additions and 594 deletions
  1. 48 74
      django/contrib/admindocs/views.py
  2. 1 1
      django/template/__init__.py
  3. 60 0
      django/template/backends/django.py
  4. 15 396
      django/template/base.py
  5. 2 1
      django/template/defaultfilters.py
  6. 47 33
      django/template/defaulttags.py
  7. 24 1
      django/template/engine.py
  8. 327 0
      django/template/library.py
  9. 2 2
      django/template/loader_tags.py
  10. 0 3
      django/test/signals.py
  11. 14 3
      docs/howto/custom-template-tags.txt
  12. 29 1
      docs/ref/templates/api.txt
  13. 26 0
      docs/releases/1.9.txt
  14. 28 0
      docs/topics/templates.txt
  15. 0 0
      tests/template_backends/apps/__init__.py
  16. 0 0
      tests/template_backends/apps/good/__init__.py
  17. 0 0
      tests/template_backends/apps/good/templatetags/__init__.py
  18. 0 0
      tests/template_backends/apps/good/templatetags/empty.py
  19. 3 0
      tests/template_backends/apps/good/templatetags/good_tags.py
  20. 3 0
      tests/template_backends/apps/good/templatetags/override.py
  21. 0 0
      tests/template_backends/apps/good/templatetags/subpackage/__init__.py
  22. 3 0
      tests/template_backends/apps/good/templatetags/subpackage/tags.py
  23. 0 0
      tests/template_backends/apps/importerror/__init__.py
  24. 0 0
      tests/template_backends/apps/importerror/templatetags/__init__.py
  25. 1 0
      tests/template_backends/apps/importerror/templatetags/broken_tags.py
  26. 77 1
      tests/template_backends/test_django.py
  27. 0 0
      tests/template_tests/broken_tag.py
  28. 5 1
      tests/template_tests/syntax_tests/test_cache.py
  29. 1 0
      tests/template_tests/syntax_tests/test_cycle.py
  30. 1 0
      tests/template_tests/syntax_tests/test_extends.py
  31. 1 0
      tests/template_tests/syntax_tests/test_firstof.py
  32. 1 0
      tests/template_tests/syntax_tests/test_for.py
  33. 4 0
      tests/template_tests/syntax_tests/test_i18n.py
  34. 1 0
      tests/template_tests/syntax_tests/test_if_changed.py
  35. 1 0
      tests/template_tests/syntax_tests/test_include.py
  36. 1 0
      tests/template_tests/syntax_tests/test_invalid_string.py
  37. 14 10
      tests/template_tests/syntax_tests/test_load.py
  38. 1 0
      tests/template_tests/syntax_tests/test_simple_tag.py
  39. 1 0
      tests/template_tests/syntax_tests/test_static.py
  40. 1 0
      tests/template_tests/syntax_tests/test_width_ratio.py
  41. 0 1
      tests/template_tests/templatetags/subpackage/echo_invalid.py
  42. 22 0
      tests/template_tests/templatetags/testtags.py
  43. 34 23
      tests/template_tests/test_custom.py
  44. 4 1
      tests/template_tests/test_engine.py
  45. 132 0
      tests/template_tests/test_library.py
  46. 1 1
      tests/template_tests/test_nodelist.py
  47. 2 1
      tests/template_tests/test_parser.py
  48. 4 1
      tests/template_tests/tests.py
  49. 7 39
      tests/template_tests/utils.py

+ 48 - 74
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"""

+ 1 - 1
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')

+ 60 - 0
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]

+ 15 - 396
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')

+ 2 - 1
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()
 

+ 47 - 33
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()
 
 

+ 24 - 1
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)

+ 327 - 0
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,
+        )

+ 2 - 2
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()
 

+ 0 - 3
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()

+ 14 - 3
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

+ 29 - 1
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

+ 26 - 0
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
 ~~~~~~~~~~~~~
 

+ 28 - 0
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

+ 0 - 0
tests/template_backends/apps/__init__.py


+ 0 - 0
tests/template_backends/apps/good/__init__.py


+ 0 - 0
tests/template_backends/apps/good/templatetags/__init__.py


+ 0 - 0
tests/template_backends/apps/good/templatetags/empty.py


+ 3 - 0
tests/template_backends/apps/good/templatetags/good_tags.py

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

+ 3 - 0
tests/template_backends/apps/good/templatetags/override.py

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

+ 0 - 0
tests/template_backends/apps/good/templatetags/subpackage/__init__.py


+ 3 - 0
tests/template_backends/apps/good/templatetags/subpackage/tags.py

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

+ 0 - 0
tests/template_backends/apps/importerror/__init__.py


+ 0 - 0
tests/template_backends/apps/importerror/templatetags/__init__.py


+ 1 - 0
tests/template_backends/apps/importerror/templatetags/broken_tags.py

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

+ 77 - 1
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',
+            ]
+        )

+ 0 - 0
tests/template_tests/templatetags/broken_tag.py → tests/template_tests/broken_tag.py


+ 5 - 1
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 4 - 0
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 14 - 10
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')

+ 1 - 0
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):

+ 1 - 0
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):

+ 1 - 0
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):

+ 0 - 1
tests/template_tests/templatetags/subpackage/echo_invalid.py

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

+ 22 - 0
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()

+ 34 - 23
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)

+ 4 - 1
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(

+ 132 - 0
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, '')

+ 1 - 1
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:

+ 2 - 1
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)

+ 4 - 1
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 %}')

+ 7 - 39
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