summaryrefslogtreecommitdiff
path: root/django/template/engine.py
blob: 62fa223e3c253090420a518472996174c67a26a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import warnings

from django.core.exceptions import ImproperlyConfigured
from django.utils import lru_cache, six
from django.utils.deprecation import RemovedInDjango110Warning
from django.utils.functional import cached_property
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()
_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', libraries=None, builtins=None):
        if dirs is None:
            dirs = []
        if allowed_include_roots is None:
            allowed_include_roots = []
        if context_processors is None:
            context_processors = []
        if loaders is None:
            loaders = ['django.template.loaders.filesystem.Loader']
            if app_dirs:
                loaders += ['django.template.loaders.app_directories.Loader']
        else:
            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(
                "allowed_include_roots must be a tuple, not a string.")

        self.dirs = dirs
        self.app_dirs = app_dirs
        self.allowed_include_roots = allowed_include_roots
        self.context_processors = context_processors
        self.debug = debug
        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()
    def get_default():
        """
        When only one DjangoTemplates backend is configured, returns it.

        Raises ImproperlyConfigured otherwise.

        This is required for preserving historical APIs that rely on a
        globally available, implicitly configured engine such as:

        >>> from django.template import Context, Template
        >>> template = Template("Hello {{ name }}!")
        >>> context = Context({'name': "world"})
        >>> template.render(context)
        'Hello world!'
        """
        # Since Engine is imported in django.template and since
        # DjangoTemplates is a wrapper around this Engine class,
        # local imports are required to avoid import loops.
        from django.template import engines
        from django.template.backends.django import DjangoTemplates
        django_engines = [engine for engine in engines.all()
                          if isinstance(engine, DjangoTemplates)]
        if len(django_engines) == 1:
            # Unwrap the Engine instance inside DjangoTemplates
            return django_engines[0].engine
        elif len(django_engines) == 0:
            raise ImproperlyConfigured(
                "No DjangoTemplates backend is configured.")
        else:
            raise ImproperlyConfigured(
                "Several DjangoTemplates backends are configured. "
                "You must select one explicitly.")

    @cached_property
    def template_context_processors(self):
        context_processors = _builtin_context_processors
        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)

    def get_template_loaders(self, template_loaders):
        loaders = []
        for template_loader in template_loaders:
            loader = self.find_template_loader(template_loader)
            if loader is not None:
                loaders.append(loader)
        return loaders

    def find_template_loader(self, loader):
        if isinstance(loader, (tuple, list)):
            args = list(loader[1:])
            loader = loader[0]
        else:
            args = []

        if isinstance(loader, six.string_types):
            loader_class = import_string(loader)

            if getattr(loader_class, '_accepts_engine_in_init', False):
                args.insert(0, self)
            else:
                warnings.warn(
                    "%s inherits from django.template.loader.BaseLoader "
                    "instead of django.template.loaders.base.Loader. " %
                    loader, RemovedInDjango110Warning, stacklevel=2)

            return loader_class(*args)
        else:
            raise ImproperlyConfigured(
                "Invalid value in template loaders configuration: %r" % loader)

    def find_template(self, name, dirs=None, skip=None):
        tried = []
        for loader in self.template_loaders:
            if loader.supports_recursion:
                try:
                    template = loader.get_template(
                        name, template_dirs=dirs, skip=skip,
                    )
                    return template, template.origin
                except TemplateDoesNotExist as e:
                    tried.extend(e.tried)
            else:
                # RemovedInDjango20Warning: Use old api for non-recursive
                # loaders.
                try:
                    return loader(name, dirs)
                except TemplateDoesNotExist:
                    pass
        raise TemplateDoesNotExist(name, tried=tried)

    def from_string(self, template_code):
        """
        Returns a compiled Template object for the given template code,
        handling template inheritance recursively.
        """
        return Template(template_code, engine=self)

    def get_template(self, template_name, dirs=_dirs_undefined):
        """
        Returns a compiled Template object for the given template name,
        handling template inheritance recursively.
        """
        if dirs is _dirs_undefined:
            dirs = None
        else:
            warnings.warn(
                "The dirs argument of get_template is deprecated.",
                RemovedInDjango110Warning, stacklevel=2)

        template, origin = self.find_template(template_name, dirs)
        if not hasattr(template, 'render'):
            # template needs to be compiled
            template = Template(template, origin, template_name, engine=self)
        return template

    # This method was originally a function defined in django.template.loader.
    # It was moved here in Django 1.8 when encapsulating the Django template
    # engine in this Engine class. It's still called by deprecated code but it
    # will be removed in Django 1.10. It's superseded by a new render_to_string
    # function in django.template.loader.

    def render_to_string(self, template_name, context=None,
                         context_instance=_context_instance_undefined,
                         dirs=_dirs_undefined,
                         dictionary=_dictionary_undefined):
        if context_instance is _context_instance_undefined:
            context_instance = None
        else:
            warnings.warn(
                "The context_instance argument of render_to_string is "
                "deprecated.", RemovedInDjango110Warning, stacklevel=3)
        if dirs is _dirs_undefined:
            # Do not set dirs to None here to avoid triggering the deprecation
            # warning in select_template or get_template.
            pass
        else:
            warnings.warn(
                "The dirs argument of render_to_string is deprecated.",
                RemovedInDjango110Warning, stacklevel=3)
        if dictionary is _dictionary_undefined:
            dictionary = None
        else:
            warnings.warn(
                "The dictionary argument of render_to_string was renamed to "
                "context.", RemovedInDjango110Warning, stacklevel=3)
            context = dictionary

        if isinstance(template_name, (list, tuple)):
            t = self.select_template(template_name, dirs)
        else:
            t = self.get_template(template_name, dirs)
        if not context_instance:
            # Django < 1.8 accepted a Context in `context` even though that's
            # unintended. Preserve this ability but don't rewrap `context`.
            if isinstance(context, Context):
                return t.render(context)
            else:
                return t.render(Context(context))
        if not context:
            return t.render(context_instance)
        # Add the context to the context stack, ensuring it gets removed again
        # to keep the context_instance in the same state it started in.
        with context_instance.push(context):
            return t.render(context_instance)

    def select_template(self, template_name_list, dirs=_dirs_undefined):
        """
        Given a list of template names, returns the first that can be loaded.
        """
        if dirs is _dirs_undefined:
            # Do not set dirs to None here to avoid triggering the deprecation
            # warning in get_template.
            pass
        else:
            warnings.warn(
                "The dirs argument of select_template is deprecated.",
                RemovedInDjango110Warning, stacklevel=3)

        if not template_name_list:
            raise TemplateDoesNotExist("No template names provided")
        not_found = []
        for template_name in template_name_list:
            try:
                return self.get_template(template_name, dirs)
            except TemplateDoesNotExist as exc:
                if exc.args[0] not in not_found:
                    not_found.append(exc.args[0])
                continue
        # If we get here, none of the templates could be loaded
        raise TemplateDoesNotExist(', '.join(not_found))