1# -*- coding: utf-8 -*-
2"""
3    jinja2.loaders
4    ~~~~~~~~~~~~~~
5
6    Jinja loader classes.
7
8    :copyright: (c) 2010 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
153    def __init__(self, searchpath, encoding='utf-8'):
154        if isinstance(searchpath, string_types):
155            searchpath = [searchpath]
156        self.searchpath = list(searchpath)
157        self.encoding = encoding
158
159    def get_source(self, environment, template):
160        pieces = split_template_path(template)
161        for searchpath in self.searchpath:
162            filename = path.join(searchpath, *pieces)
163            f = open_if_exists(filename)
164            if f is None:
165                continue
166            try:
167                contents = f.read().decode(self.encoding)
168            finally:
169                f.close()
170
171            mtime = path.getmtime(filename)
172            def uptodate():
173                try:
174                    return path.getmtime(filename) == mtime
175                except OSError:
176                    return False
177            return contents, filename, uptodate
178        raise TemplateNotFound(template)
179
180    def list_templates(self):
181        found = set()
182        for searchpath in self.searchpath:
183            for dirpath, dirnames, filenames in os.walk(searchpath):
184                for filename in filenames:
185                    template = os.path.join(dirpath, filename) \
186                        [len(searchpath):].strip(os.path.sep) \
187                                          .replace(os.path.sep, '/')
188                    if template[:2] == './':
189                        template = template[2:]
190                    if template not in found:
191                        found.add(template)
192        return sorted(found)
193
194
195class PackageLoader(BaseLoader):
196    """Load templates from python eggs or packages.  It is constructed with
197    the name of the python package and the path to the templates in that
198    package::
199
200        loader = PackageLoader('mypackage', 'views')
201
202    If the package path is not given, ``'templates'`` is assumed.
203
204    Per default the template encoding is ``'utf-8'`` which can be changed
205    by setting the `encoding` parameter to something else.  Due to the nature
206    of eggs it's only possible to reload templates if the package was loaded
207    from the file system and not a zip file.
208    """
209
210    def __init__(self, package_name, package_path='templates',
211                 encoding='utf-8'):
212        from pkg_resources import DefaultProvider, ResourceManager, \
213                                  get_provider
214        provider = get_provider(package_name)
215        self.encoding = encoding
216        self.manager = ResourceManager()
217        self.filesystem_bound = isinstance(provider, DefaultProvider)
218        self.provider = provider
219        self.package_path = package_path
220
221    def get_source(self, environment, template):
222        pieces = split_template_path(template)
223        p = '/'.join((self.package_path,) + tuple(pieces))
224        if not self.provider.has_resource(p):
225            raise TemplateNotFound(template)
226
227        filename = uptodate = None
228        if self.filesystem_bound:
229            filename = self.provider.get_resource_filename(self.manager, p)
230            mtime = path.getmtime(filename)
231            def uptodate():
232                try:
233                    return path.getmtime(filename) == mtime
234                except OSError:
235                    return False
236
237        source = self.provider.get_resource_string(self.manager, p)
238        return source.decode(self.encoding), filename, uptodate
239
240    def list_templates(self):
241        path = self.package_path
242        if path[:2] == './':
243            path = path[2:]
244        elif path == '.':
245            path = ''
246        offset = len(path)
247        results = []
248        def _walk(path):
249            for filename in self.provider.resource_listdir(path):
250                fullname = path + '/' + filename
251                if self.provider.resource_isdir(fullname):
252                    _walk(fullname)
253                else:
254                    results.append(fullname[offset:].lstrip('/'))
255        _walk(path)
256        results.sort()
257        return results
258
259
260class DictLoader(BaseLoader):
261    """Loads a template from a python dict.  It's passed a dict of unicode
262    strings bound to template names.  This loader is useful for unittesting:
263
264    >>> loader = DictLoader({'index.html': 'source here'})
265
266    Because auto reloading is rarely useful this is disabled per default.
267    """
268
269    def __init__(self, mapping):
270        self.mapping = mapping
271
272    def get_source(self, environment, template):
273        if template in self.mapping:
274            source = self.mapping[template]
275            return source, None, lambda: source == self.mapping.get(template)
276        raise TemplateNotFound(template)
277
278    def list_templates(self):
279        return sorted(self.mapping)
280
281
282class FunctionLoader(BaseLoader):
283    """A loader that is passed a function which does the loading.  The
284    function becomes the name of the template passed and has to return either
285    an unicode string with the template source, a tuple in the form ``(source,
286    filename, uptodatefunc)`` or `None` if the template does not exist.
287
288    >>> def load_template(name):
289    ...     if name == 'index.html':
290    ...         return '...'
291    ...
292    >>> loader = FunctionLoader(load_template)
293
294    The `uptodatefunc` is a function that is called if autoreload is enabled
295    and has to return `True` if the template is still up to date.  For more
296    details have a look at :meth:`BaseLoader.get_source` which has the same
297    return value.
298    """
299
300    def __init__(self, load_func):
301        self.load_func = load_func
302
303    def get_source(self, environment, template):
304        rv = self.load_func(template)
305        if rv is None:
306            raise TemplateNotFound(template)
307        elif isinstance(rv, string_types):
308            return rv, None, None
309        return rv
310
311
312class PrefixLoader(BaseLoader):
313    """A loader that is passed a dict of loaders where each loader is bound
314    to a prefix.  The prefix is delimited from the template by a slash per
315    default, which can be changed by setting the `delimiter` argument to
316    something else::
317
318        loader = PrefixLoader({
319            'app1':     PackageLoader('mypackage.app1'),
320            'app2':     PackageLoader('mypackage.app2')
321        })
322
323    By loading ``'app1/index.html'`` the file from the app1 package is loaded,
324    by loading ``'app2/index.html'`` the file from the second.
325    """
326
327    def __init__(self, mapping, delimiter='/'):
328        self.mapping = mapping
329        self.delimiter = delimiter
330
331    def get_loader(self, template):
332        try:
333            prefix, name = template.split(self.delimiter, 1)
334            loader = self.mapping[prefix]
335        except (ValueError, KeyError):
336            raise TemplateNotFound(template)
337        return loader, name
338
339    def get_source(self, environment, template):
340        loader, name = self.get_loader(template)
341        try:
342            return loader.get_source(environment, name)
343        except TemplateNotFound:
344            # re-raise the exception with the correct fileame here.
345            # (the one that includes the prefix)
346            raise TemplateNotFound(template)
347
348    @internalcode
349    def load(self, environment, name, globals=None):
350        loader, local_name = self.get_loader(name)
351        try:
352            return loader.load(environment, local_name)
353        except TemplateNotFound:
354            # re-raise the exception with the correct fileame here.
355            # (the one that includes the prefix)
356            raise TemplateNotFound(name)
357
358    def list_templates(self):
359        result = []
360        for prefix, loader in iteritems(self.mapping):
361            for template in loader.list_templates():
362                result.append(prefix + self.delimiter + template)
363        return result
364
365
366class ChoiceLoader(BaseLoader):
367    """This loader works like the `PrefixLoader` just that no prefix is
368    specified.  If a template could not be found by one loader the next one
369    is tried.
370
371    >>> loader = ChoiceLoader([
372    ...     FileSystemLoader('/path/to/user/templates'),
373    ...     FileSystemLoader('/path/to/system/templates')
374    ... ])
375
376    This is useful if you want to allow users to override builtin templates
377    from a different location.
378    """
379
380    def __init__(self, loaders):
381        self.loaders = loaders
382
383    def get_source(self, environment, template):
384        for loader in self.loaders:
385            try:
386                return loader.get_source(environment, template)
387            except TemplateNotFound:
388                pass
389        raise TemplateNotFound(template)
390
391    @internalcode
392    def load(self, environment, name, globals=None):
393        for loader in self.loaders:
394            try:
395                return loader.load(environment, name, globals)
396            except TemplateNotFound:
397                pass
398        raise TemplateNotFound(name)
399
400    def list_templates(self):
401        found = set()
402        for loader in self.loaders:
403            found.update(loader.list_templates())
404        return sorted(found)
405
406
407class _TemplateModule(ModuleType):
408    """Like a normal module but with support for weak references"""
409
410
411class ModuleLoader(BaseLoader):
412    """This loader loads templates from precompiled templates.
413
414    Example usage:
415
416    >>> loader = ChoiceLoader([
417    ...     ModuleLoader('/path/to/compiled/templates'),
418    ...     FileSystemLoader('/path/to/templates')
419    ... ])
420
421    Templates can be precompiled with :meth:`Environment.compile_templates`.
422    """
423
424    has_source_access = False
425
426    def __init__(self, path):
427        package_name = '_jinja2_module_templates_%x' % id(self)
428
429        # create a fake module that looks for the templates in the
430        # path given.
431        mod = _TemplateModule(package_name)
432        if isinstance(path, string_types):
433            path = [path]
434        else:
435            path = list(path)
436        mod.__path__ = path
437
438        sys.modules[package_name] = weakref.proxy(mod,
439            lambda x: sys.modules.pop(package_name, None))
440
441        # the only strong reference, the sys.modules entry is weak
442        # so that the garbage collector can remove it once the
443        # loader that created it goes out of business.
444        self.module = mod
445        self.package_name = package_name
446
447    @staticmethod
448    def get_template_key(name):
449        return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
450
451    @staticmethod
452    def get_module_filename(name):
453        return ModuleLoader.get_template_key(name) + '.py'
454
455    @internalcode
456    def load(self, environment, name, globals=None):
457        key = self.get_template_key(name)
458        module = '%s.%s' % (self.package_name, key)
459        mod = getattr(self.module, module, None)
460        if mod is None:
461            try:
462                mod = __import__(module, None, None, ['root'])
463            except ImportError:
464                raise TemplateNotFound(name)
465
466            # remove the entry from sys.modules, we only want the attribute
467            # on the module object we have stored on the loader.
468            sys.modules.pop(module, None)
469
470        return environment.template_class.from_module_dict(
471            environment, mod.__dict__, globals)
472