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 try:
     17     from hashlib import sha1
     18 except ImportError:
     19     from sha import new as sha1
     20 from jinja2.exceptions import TemplateNotFound
     21 from jinja2.utils import LRUCache, open_if_exists, internalcode
     22 
     23 
     24 def 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 
     39 class 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 
    140 class 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 
    197 class 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 
    262 class 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 
    284 class 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 
    314 class 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 
    354 class 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 
    386 class _TemplateModule(ModuleType):
    387     """Like a normal module but with support for weak references"""
    388 
    389 
    390 class 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