1# -*- coding: utf-8 -*-
2"""
3    jinja2.loaders
4    ~~~~~~~~~~~~~~
5
6    Jinja loader classes.
7
8    :copyright: (c) 2017 by the Jinja Team.
9    :license: BSD, see LICENSE for more details.
10"""
11import os
12import sys
13import weakref
14from types import ModuleType
15from os import path
16from hashlib import sha1
17from jinja2.exceptions import TemplateNotFound
18from jinja2.utils import open_if_exists, internalcode
19from jinja2._compat import string_types, iteritems
20
21
22def split_template_path(template):
23    """Split a path into segments and perform a sanity check.  If it detects
24    '..' in the path it will raise a `TemplateNotFound` error.
25    """
26    pieces = []
27    for piece in template.split('/'):
28        if path.sep in piece \
29           or (path.altsep and path.altsep in piece) or \
30           piece == path.pardir:
31            raise TemplateNotFound(template)
32        elif piece and piece != '.':
33            pieces.append(piece)
34    return pieces
35
36
37class BaseLoader(object):
38    """Baseclass for all loaders.  Subclass this and override `get_source` to
39    implement a custom loading mechanism.  The environment provides a
40    `get_template` method that calls the loader's `load` method to get the
41    :class:`Template` object.
42
43    A very basic example for a loader that looks up templates on the file
44    system could look like this::
45
46        from jinja2 import BaseLoader, TemplateNotFound
47        from os.path import join, exists, getmtime
48
49        class MyLoader(BaseLoader):
50
51            def __init__(self, path):
52                self.path = path
53
54            def get_source(self, environment, template):
55                path = join(self.path, template)
56                if not exists(path):
57                    raise TemplateNotFound(template)
58                mtime = getmtime(path)
59                with file(path) as f:
60                    source = f.read().decode('utf-8')
61                return source, path, lambda: mtime == getmtime(path)
62    """
63
64    #: if set to `False` it indicates that the loader cannot provide access
65    #: to the source of templates.
66    #:
67    #: .. versionadded:: 2.4
68    has_source_access = True
69
70    def get_source(self, environment, template):
71        """Get the template source, filename and reload helper for a template.
72        It's passed the environment and template name and has to return a
73        tuple in the form ``(source, filename, uptodate)`` or raise a
74        `TemplateNotFound` error if it can't locate the template.
75
76        The source part of the returned tuple must be the source of the
77        template as unicode string or a ASCII bytestring.  The filename should
78        be the name of the file on the filesystem if it was loaded from there,
79        otherwise `None`.  The filename is used by python for the tracebacks
80        if no loader extension is used.
81
82        The last item in the tuple is the `uptodate` function.  If auto
83        reloading is enabled it's always called to check if the template
84        changed.  No arguments are passed so the function must store the
85        old state somewhere (for example in a closure).  If it returns `False`
86        the template will be reloaded.
87        """
88        if not self.has_source_access:
89            raise RuntimeError('%s cannot provide access to the source' %
90                               self.__class__.__name__)
91        raise TemplateNotFound(template)
92
93    def list_templates(self):
94        """Iterates over all templates.  If the loader does not support that
95        it should raise a :exc:`TypeError` which is the default behavior.
96        """
97        raise TypeError('this loader cannot iterate over all templates')
98
99    @internalcode
100    def load(self, environment, name, globals=None):
101        """Loads a template.  This method looks up the template in the cache
102        or loads one by calling :meth:`get_source`.  Subclasses should not
103        override this method as loaders working on collections of other
104        loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
105        will not call this method but `get_source` directly.
106        """
107        code = None
108        if globals is None:
109            globals = {}
110
111        # first we try to get the source for this template together
112        # with the filename and the uptodate function.
113        source, filename, uptodate = self.get_source(environment, name)
114
115        # try to load the code from the bytecode cache if there is a
116        # bytecode cache configured.
117        bcc = environment.bytecode_cache
118        if bcc is not None:
119            bucket = bcc.get_bucket(environment, name, filename, source)
120            code = bucket.code
121
122        # if we don't have code so far (not cached, no longer up to
123        # date) etc. we compile the template
124        if code is None:
125            code = environment.compile(source, name, filename)
126
127        # if the bytecode cache is available and the bucket doesn't
128        # have a code so far, we give the bucket the new code and put
129        # it back to the bytecode cache.
130        if bcc is not None and bucket.code is None:
131            bucket.code = code
132            bcc.set_bucket(bucket)
133
134        return environment.template_class.from_code(environment, code,
135                                                    globals, uptodate)
136
137
138class FileSystemLoader(BaseLoader):
139    """Loads templates from the file system.  This loader can find templates
140    in folders on the file system and is the preferred way to load them.
141
142    The loader takes the path to the templates as string, or if multiple
143    locations are wanted a list of them which is then looked up in the
144    given order::
145
146    >>> loader = FileSystemLoader('/path/to/templates')
147    >>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
148
149    Per default the template encoding is ``'utf-8'`` which can be changed
150    by setting the `encoding` parameter to something else.
151
152    To follow symbolic links, set the *followlinks* parameter to ``True``::
153
154    >>> loader = FileSystemLoader('/path/to/templates', followlinks=True)
155
156    .. versionchanged:: 2.8+
157       The *followlinks* parameter was added.
158    """
159
160    def __init__(self, searchpath, encoding='utf-8', followlinks=False):
161        if isinstance(searchpath, string_types):
162            searchpath = [searchpath]
163        self.searchpath = list(searchpath)
164        self.encoding = encoding
165        self.followlinks = followlinks
166
167    def get_source(self, environment, template):
168        pieces = split_template_path(template)
169        for searchpath in self.searchpath:
170            filename = path.join(searchpath, *pieces)
171            f = open_if_exists(filename)
172            if f is None:
173                continue
174            try:
175                contents = f.read().decode(self.encoding)
176            finally:
177                f.close()
178
179            mtime = path.getmtime(filename)
180
181            def uptodate():
182                try:
183                    return path.getmtime(filename) == mtime
184                except OSError:
185                    return False
186            return contents, filename, uptodate
187        raise TemplateNotFound(template)
188
189    def list_templates(self):
190        found = set()
191        for searchpath in self.searchpath:
192            walk_dir = os.walk(searchpath, followlinks=self.followlinks)
193            for dirpath, dirnames, filenames in walk_dir:
194                for filename in filenames:
195                    template = os.path.join(dirpath, filename) \
196                        [len(searchpath):].strip(os.path.sep) \
197                                          .replace(os.path.sep, '/')
198                    if template[:2] == './':
199                        template = template[2:]
200                    if template not in found:
201                        found.add(template)
202        return sorted(found)
203
204
205class PackageLoader(BaseLoader):
206    """Load templates from python eggs or packages.  It is constructed with
207    the name of the python package and the path to the templates in that
208    package::
209
210        loader = PackageLoader('mypackage', 'views')
211
212    If the package path is not given, ``'templates'`` is assumed.
213
214    Per default the template encoding is ``'utf-8'`` which can be changed
215    by setting the `encoding` parameter to something else.  Due to the nature
216    of eggs it's only possible to reload templates if the package was loaded
217    from the file system and not a zip file.
218    """
219
220    def __init__(self, package_name, package_path='templates',
221                 encoding='utf-8'):
222        from pkg_resources import DefaultProvider, ResourceManager, \
223                                  get_provider
224        provider = get_provider(package_name)
225        self.encoding = encoding
226        self.manager = ResourceManager()
227        self.filesystem_bound = isinstance(provider, DefaultProvider)
228        self.provider = provider
229        self.package_path = package_path
230
231    def get_source(self, environment, template):
232        pieces = split_template_path(template)
233        p = '/'.join((self.package_path,) + tuple(pieces))
234        if not self.provider.has_resource(p):
235            raise TemplateNotFound(template)
236
237        filename = uptodate = None
238        if self.filesystem_bound:
239            filename = self.provider.get_resource_filename(self.manager, p)
240            mtime = path.getmtime(filename)
241            def uptodate():
242                try:
243                    return path.getmtime(filename) == mtime
244                except OSError:
245                    return False
246
247        source = self.provider.get_resource_string(self.manager, p)
248        return source.decode(self.encoding), filename, uptodate
249
250    def list_templates(self):
251        path = self.package_path
252        if path[:2] == './':
253            path = path[2:]
254        elif path == '.':
255            path = ''
256        offset = len(path)
257        results = []
258        def _walk(path):
259            for filename in self.provider.resource_listdir(path):
260                fullname = path + '/' + filename
261                if self.provider.resource_isdir(fullname):
262                    _walk(fullname)
263                else:
264                    results.append(fullname[offset:].lstrip('/'))
265        _walk(path)
266        results.sort()
267        return results
268
269
270class DictLoader(BaseLoader):
271    """Loads a template from a python dict.  It's passed a dict of unicode
272    strings bound to template names.  This loader is useful for unittesting:
273
274    >>> loader = DictLoader({'index.html': 'source here'})
275
276    Because auto reloading is rarely useful this is disabled per default.
277    """
278
279    def __init__(self, mapping):
280        self.mapping = mapping
281
282    def get_source(self, environment, template):
283        if template in self.mapping:
284            source = self.mapping[template]
285            return source, None, lambda: source == self.mapping.get(template)
286        raise TemplateNotFound(template)
287
288    def list_templates(self):
289        return sorted(self.mapping)
290
291
292class FunctionLoader(BaseLoader):
293    """A loader that is passed a function which does the loading.  The
294    function receives the name of the template and has to return either
295    an unicode string with the template source, a tuple in the form ``(source,
296    filename, uptodatefunc)`` or `None` if the template does not exist.
297
298    >>> def load_template(name):
299    ...     if name == 'index.html':
300    ...         return '...'
301    ...
302    >>> loader = FunctionLoader(load_template)
303
304    The `uptodatefunc` is a function that is called if autoreload is enabled
305    and has to return `True` if the template is still up to date.  For more
306    details have a look at :meth:`BaseLoader.get_source` which has the same
307    return value.
308    """
309
310    def __init__(self, load_func):
311        self.load_func = load_func
312
313    def get_source(self, environment, template):
314        rv = self.load_func(template)
315        if rv is None:
316            raise TemplateNotFound(template)
317        elif isinstance(rv, string_types):
318            return rv, None, None
319        return rv
320
321
322class PrefixLoader(BaseLoader):
323    """A loader that is passed a dict of loaders where each loader is bound
324    to a prefix.  The prefix is delimited from the template by a slash per
325    default, which can be changed by setting the `delimiter` argument to
326    something else::
327
328        loader = PrefixLoader({
329            'app1':     PackageLoader('mypackage.app1'),
330            'app2':     PackageLoader('mypackage.app2')
331        })
332
333    By loading ``'app1/index.html'`` the file from the app1 package is loaded,
334    by loading ``'app2/index.html'`` the file from the second.
335    """
336
337    def __init__(self, mapping, delimiter='/'):
338        self.mapping = mapping
339        self.delimiter = delimiter
340
341    def get_loader(self, template):
342        try:
343            prefix, name = template.split(self.delimiter, 1)
344            loader = self.mapping[prefix]
345        except (ValueError, KeyError):
346            raise TemplateNotFound(template)
347        return loader, name
348
349    def get_source(self, environment, template):
350        loader, name = self.get_loader(template)
351        try:
352            return loader.get_source(environment, name)
353        except TemplateNotFound:
354            # re-raise the exception with the correct filename here.
355            # (the one that includes the prefix)
356            raise TemplateNotFound(template)
357
358    @internalcode
359    def load(self, environment, name, globals=None):
360        loader, local_name = self.get_loader(name)
361        try:
362            return loader.load(environment, local_name, globals)
363        except TemplateNotFound:
364            # re-raise the exception with the correct filename here.
365            # (the one that includes the prefix)
366            raise TemplateNotFound(name)
367
368    def list_templates(self):
369        result = []
370        for prefix, loader in iteritems(self.mapping):
371            for template in loader.list_templates():
372                result.append(prefix + self.delimiter + template)
373        return result
374
375
376class ChoiceLoader(BaseLoader):
377    """This loader works like the `PrefixLoader` just that no prefix is
378    specified.  If a template could not be found by one loader the next one
379    is tried.
380
381    >>> loader = ChoiceLoader([
382    ...     FileSystemLoader('/path/to/user/templates'),
383    ...     FileSystemLoader('/path/to/system/templates')
384    ... ])
385
386    This is useful if you want to allow users to override builtin templates
387    from a different location.
388    """
389
390    def __init__(self, loaders):
391        self.loaders = loaders
392
393    def get_source(self, environment, template):
394        for loader in self.loaders:
395            try:
396                return loader.get_source(environment, template)
397            except TemplateNotFound:
398                pass
399        raise TemplateNotFound(template)
400
401    @internalcode
402    def load(self, environment, name, globals=None):
403        for loader in self.loaders:
404            try:
405                return loader.load(environment, name, globals)
406            except TemplateNotFound:
407                pass
408        raise TemplateNotFound(name)
409
410    def list_templates(self):
411        found = set()
412        for loader in self.loaders:
413            found.update(loader.list_templates())
414        return sorted(found)
415
416
417class _TemplateModule(ModuleType):
418    """Like a normal module but with support for weak references"""
419
420
421class ModuleLoader(BaseLoader):
422    """This loader loads templates from precompiled templates.
423
424    Example usage:
425
426    >>> loader = ChoiceLoader([
427    ...     ModuleLoader('/path/to/compiled/templates'),
428    ...     FileSystemLoader('/path/to/templates')
429    ... ])
430
431    Templates can be precompiled with :meth:`Environment.compile_templates`.
432    """
433
434    has_source_access = False
435
436    def __init__(self, path):
437        package_name = '_jinja2_module_templates_%x' % id(self)
438
439        # create a fake module that looks for the templates in the
440        # path given.
441        mod = _TemplateModule(package_name)
442        if isinstance(path, string_types):
443            path = [path]
444        else:
445            path = list(path)
446        mod.__path__ = path
447
448        sys.modules[package_name] = weakref.proxy(mod,
449            lambda x: sys.modules.pop(package_name, None))
450
451        # the only strong reference, the sys.modules entry is weak
452        # so that the garbage collector can remove it once the
453        # loader that created it goes out of business.
454        self.module = mod
455        self.package_name = package_name
456
457    @staticmethod
458    def get_template_key(name):
459        return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
460
461    @staticmethod
462    def get_module_filename(name):
463        return ModuleLoader.get_template_key(name) + '.py'
464
465    @internalcode
466    def load(self, environment, name, globals=None):
467        key = self.get_template_key(name)
468        module = '%s.%s' % (self.package_name, key)
469        mod = getattr(self.module, module, None)
470        if mod is None:
471            try:
472                mod = __import__(module, None, None, ['root'])
473            except ImportError:
474                raise TemplateNotFound(name)
475
476            # remove the entry from sys.modules, we only want the attribute
477            # on the module object we have stored on the loader.
478            sys.modules.pop(module, None)
479
480        return environment.template_class.from_module_dict(
481            environment, mod.__dict__, globals)
482