Commit 655f5249 authored by Preston Timmons's avatar Preston Timmons
Browse files

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.
parent 7b8008a0
Loading
Loading
Loading
Loading
+48 −74
Original line number Diff line number Diff line
@@ -12,12 +12,7 @@ from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.db import models
from django.http import Http404
from django.template.base import (
    InvalidTemplateLibrary, builtins, get_library, get_templatetags_modules,
    libraries,
)
from django.template.engine import Engine
from django.utils._os import upath
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
@@ -60,11 +55,15 @@ class TemplateTagIndexView(BaseAdminDocsView):
    template_name = 'admin_doc/template_tag_index.html'

    def get_context_data(self, **kwargs):
        load_all_installed_template_libraries()

        tags = []
        app_libs = list(libraries.items())
        builtin_libs = [(None, lib) for lib in builtins]
        try:
            engine = Engine.get_default()
        except ImproperlyConfigured:
            # Non-trivial TEMPLATES settings aren't supported (#24125).
            pass
        else:
            app_libs = sorted(engine.template_libraries.items())
            builtin_libs = [('', lib) for lib in engine.template_builtins]
            for module_name, library in builtin_libs + app_libs:
                for tag_name, tag_func in library.tags.items():
                    title, body, metadata = utils.parse_docstring(tag_func.__doc__)
@@ -74,9 +73,6 @@ class TemplateTagIndexView(BaseAdminDocsView):
                        body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
                    for key in metadata:
                        metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
                if library in builtins:
                    tag_library = ''
                else:
                    tag_library = module_name.split('.')[-1]
                    tags.append({
                        'name': tag_name,
@@ -93,11 +89,15 @@ class TemplateFilterIndexView(BaseAdminDocsView):
    template_name = 'admin_doc/template_filter_index.html'

    def get_context_data(self, **kwargs):
        load_all_installed_template_libraries()

        filters = []
        app_libs = list(libraries.items())
        builtin_libs = [(None, lib) for lib in builtins]
        try:
            engine = Engine.get_default()
        except ImproperlyConfigured:
            # Non-trivial TEMPLATES settings aren't supported (#24125).
            pass
        else:
            app_libs = sorted(engine.template_libraries.items())
            builtin_libs = [('', lib) for lib in engine.template_builtins]
            for module_name, library in builtin_libs + app_libs:
                for filter_name, filter_func in library.filters.items():
                    title, body, metadata = utils.parse_docstring(filter_func.__doc__)
@@ -107,9 +107,6 @@ class TemplateFilterIndexView(BaseAdminDocsView):
                        body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
                    for key in metadata:
                        metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
                if library in builtins:
                    tag_library = ''
                else:
                    tag_library = module_name.split('.')[-1]
                    filters.append({
                        'name': filter_name,
@@ -320,29 +317,6 @@ class TemplateDetailView(BaseAdminDocsView):
# Helper functions #
####################

def load_all_installed_template_libraries():
    # Load/register all template tag libraries from installed apps.
    for module_name in get_templatetags_modules():
        mod = import_module(module_name)
        if not hasattr(mod, '__file__'):
            # e.g. packages installed as eggs
            continue

        try:
            libraries = [
                os.path.splitext(p)[0]
                for p in os.listdir(os.path.dirname(upath(mod.__file__)))
                if p.endswith('.py') and p[0].isalpha()
            ]
        except OSError:
            continue
        else:
            for library_name in libraries:
                try:
                    get_library(library_name)
                except InvalidTemplateLibrary:
                    pass


def get_return_data_type(func_name):
    """Return a somewhat-helpful data type given a function name"""
+1 −1
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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

File changed.

Preview size limit exceeded, changes collapsed.

Loading