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