bd435e8b086d7c2c4916c49500850f2edfa44071
[jinja2.git] / jinja2 / loaders.py
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                     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
263 class 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
285 class 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
315 class 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
355 class 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
387 class _TemplateModule(ModuleType):
388     """Like a normal module but with support for weak references"""
389
390
391 class 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)