Home | History | Annotate | Download | only in jinja2
      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 """
     11 import os
     12 import sys
     13 import weakref
     14 from types import ModuleType
     15 from os import path
     16 from hashlib import sha1
     17 from jinja2.exceptions import TemplateNotFound
     18 from jinja2.utils import open_if_exists, internalcode
     19 from jinja2._compat import string_types, iteritems
     20 
     21 
     22 def 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 
     37 class 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 
    138 class 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 
    195 class 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 
    260 class 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 
    282 class 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 
    312 class 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 
    366 class 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 
    407 class _TemplateModule(ModuleType):
    408     """Like a normal module but with support for weak references"""
    409 
    410 
    411 class 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