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