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                    for item in _walk(fullname):
255                        results.append(item)
256                else:
257                    results.append(fullname[offset:].lstrip('/'))
258        _walk(path)
259        results.sort()
260        return results
261
262
263class DictLoader(BaseLoader):
264    """Loads a template from a python dict.  It's passed a dict of unicode
265    strings bound to template names.  This loader is useful for unittesting:
266
267    >>> loader = DictLoader({'index.html': 'source here'})
268
269    Because auto reloading is rarely useful this is disabled per default.
270    """
271
272    def __init__(self, mapping):
273        self.mapping = mapping
274
275    def get_source(self, environment, template):
276        if template in self.mapping:
277            source = self.mapping[template]
278            return source, None, lambda: source != self.mapping.get(template)
279        raise TemplateNotFound(template)
280
281    def list_templates(self):
282        return sorted(self.mapping)
283
284
285class FunctionLoader(BaseLoader):
286    """A loader that is passed a function which does the loading.  The
287    function becomes the name of the template passed and has to return either
288    an unicode string with the template source, a tuple in the form ``(source,
289    filename, uptodatefunc)`` or `None` if the template does not exist.
290
291    >>> def load_template(name):
292    ...     if name == 'index.html':
293    ...         return '...'
294    ...
295    >>> loader = FunctionLoader(load_template)
296
297    The `uptodatefunc` is a function that is called if autoreload is enabled
298    and has to return `True` if the template is still up to date.  For more
299    details have a look at :meth:`BaseLoader.get_source` which has the same
300    return value.
301    """
302
303    def __init__(self, load_func):
304        self.load_func = load_func
305
306    def get_source(self, environment, template):
307        rv = self.load_func(template)
308        if rv is None:
309            raise TemplateNotFound(template)
310        elif isinstance(rv, basestring):
311            return rv, None, None
312        return rv
313
314
315class PrefixLoader(BaseLoader):
316    """A loader that is passed a dict of loaders where each loader is bound
317    to a prefix.  The prefix is delimited from the template by a slash per
318    default, which can be changed by setting the `delimiter` argument to
319    something else::
320
321        loader = PrefixLoader({
322            'app1':     PackageLoader('mypackage.app1'),
323            'app2':     PackageLoader('mypackage.app2')
324        })
325
326    By loading ``'app1/index.html'`` the file from the app1 package is loaded,
327    by loading ``'app2/index.html'`` the file from the second.
328    """
329
330    def __init__(self, mapping, delimiter='/'):
331        self.mapping = mapping
332        self.delimiter = delimiter
333
334    def get_source(self, environment, template):
335        try:
336            prefix, name = template.split(self.delimiter, 1)
337            loader = self.mapping[prefix]
338        except (ValueError, KeyError):
339            raise TemplateNotFound(template)
340        try:
341            return loader.get_source(environment, name)
342        except TemplateNotFound:
343            # re-raise the exception with the correct fileame here.
344            # (the one that includes the prefix)
345            raise TemplateNotFound(template)
346
347    def list_templates(self):
348        result = []
349        for prefix, loader in self.mapping.iteritems():
350            for template in loader.list_templates():
351                result.append(prefix + self.delimiter + template)
352        return result
353
354
355class ChoiceLoader(BaseLoader):
356    """This loader works like the `PrefixLoader` just that no prefix is
357    specified.  If a template could not be found by one loader the next one
358    is tried.
359
360    >>> loader = ChoiceLoader([
361    ...     FileSystemLoader('/path/to/user/templates'),
362    ...     FileSystemLoader('/path/to/system/templates')
363    ... ])
364
365    This is useful if you want to allow users to override builtin templates
366    from a different location.
367    """
368
369    def __init__(self, loaders):
370        self.loaders = loaders
371
372    def get_source(self, environment, template):
373        for loader in self.loaders:
374            try:
375                return loader.get_source(environment, template)
376            except TemplateNotFound:
377                pass
378        raise TemplateNotFound(template)
379
380    def list_templates(self):
381        found = set()
382        for loader in self.loaders:
383            found.update(loader.list_templates())
384        return sorted(found)
385
386
387class _TemplateModule(ModuleType):
388    """Like a normal module but with support for weak references"""
389
390
391class ModuleLoader(BaseLoader):
392    """This loader loads templates from precompiled templates.
393
394    Example usage:
395
396    >>> loader = ChoiceLoader([
397    ...     ModuleLoader('/path/to/compiled/templates'),
398    ...     FileSystemLoader('/path/to/templates')
399    ... ])
400    """
401
402    has_source_access = False
403
404    def __init__(self, path):
405        package_name = '_jinja2_module_templates_%x' % id(self)
406
407        # create a fake module that looks for the templates in the
408        # path given.
409        mod = _TemplateModule(package_name)
410        if isinstance(path, basestring):
411            path = [path]
412        else:
413            path = list(path)
414        mod.__path__ = path
415
416        sys.modules[package_name] = weakref.proxy(mod,
417            lambda x: sys.modules.pop(package_name, None))
418
419        # the only strong reference, the sys.modules entry is weak
420        # so that the garbage collector can remove it once the
421        # loader that created it goes out of business.
422        self.module = mod
423        self.package_name = package_name
424
425    @staticmethod
426    def get_template_key(name):
427        return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
428
429    @staticmethod
430    def get_module_filename(name):
431        return ModuleLoader.get_template_key(name) + '.py'
432
433    @internalcode
434    def load(self, environment, name, globals=None):
435        key = self.get_template_key(name)
436        module = '%s.%s' % (self.package_name, key)
437        mod = getattr(self.module, module, None)
438        if mod is None:
439            try:
440                mod = __import__(module, None, None, ['root'])
441            except ImportError:
442                raise TemplateNotFound(name)
443
444            # remove the entry from sys.modules, we only want the attribute
445            # on the module object we have stored on the loader.
446            sys.modules.pop(module, None)
447
448        return environment.template_class.from_module_dict(
449            environment, mod.__dict__, globals)
450