Run `./2to3.py -w jinja2`
[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, str):
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, str):
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_loader(self, 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         return loader, name
340
341     def get_source(self, environment, template):
342         loader, name = self.get_loader(template)
343         try:
344             return loader.get_source(environment, name)
345         except TemplateNotFound:
346             # re-raise the exception with the correct fileame here.
347             # (the one that includes the prefix)
348             raise TemplateNotFound(template)
349
350     @internalcode
351     def load(self, environment, name, globals=None):
352         loader, local_name = self.get_loader(name)
353         try:
354             return loader.load(environment, local_name)
355         except TemplateNotFound:
356             # re-raise the exception with the correct fileame here.
357             # (the one that includes the prefix)
358             raise TemplateNotFound(name)
359
360     def list_templates(self):
361         result = []
362         for prefix, loader in self.mapping.items():
363             for template in loader.list_templates():
364                 result.append(prefix + self.delimiter + template)
365         return result
366
367
368 class ChoiceLoader(BaseLoader):
369     """This loader works like the `PrefixLoader` just that no prefix is
370     specified.  If a template could not be found by one loader the next one
371     is tried.
372
373     >>> loader = ChoiceLoader([
374     ...     FileSystemLoader('/path/to/user/templates'),
375     ...     FileSystemLoader('/path/to/system/templates')
376     ... ])
377
378     This is useful if you want to allow users to override builtin templates
379     from a different location.
380     """
381
382     def __init__(self, loaders):
383         self.loaders = loaders
384
385     def get_source(self, environment, template):
386         for loader in self.loaders:
387             try:
388                 return loader.get_source(environment, template)
389             except TemplateNotFound:
390                 pass
391         raise TemplateNotFound(template)
392
393     @internalcode
394     def load(self, environment, name, globals=None):
395         for loader in self.loaders:
396             try:
397                 return loader.load(environment, name, globals)
398             except TemplateNotFound:
399                 pass
400         raise TemplateNotFound(name)
401
402     def list_templates(self):
403         found = set()
404         for loader in self.loaders:
405             found.update(loader.list_templates())
406         return sorted(found)
407
408
409 class _TemplateModule(ModuleType):
410     """Like a normal module but with support for weak references"""
411
412
413 class ModuleLoader(BaseLoader):
414     """This loader loads templates from precompiled templates.
415
416     Example usage:
417
418     >>> loader = ChoiceLoader([
419     ...     ModuleLoader('/path/to/compiled/templates'),
420     ...     FileSystemLoader('/path/to/templates')
421     ... ])
422
423     Templates can be precompiled with :meth:`Environment.compile_templates`.
424     """
425
426     has_source_access = False
427
428     def __init__(self, path):
429         package_name = '_jinja2_module_templates_%x' % id(self)
430
431         # create a fake module that looks for the templates in the
432         # path given.
433         mod = _TemplateModule(package_name)
434         if isinstance(path, str):
435             path = [path]
436         else:
437             path = list(path)
438         mod.__path__ = path
439
440         sys.modules[package_name] = weakref.proxy(mod,
441             lambda x: sys.modules.pop(package_name, None))
442
443         # the only strong reference, the sys.modules entry is weak
444         # so that the garbage collector can remove it once the
445         # loader that created it goes out of business.
446         self.module = mod
447         self.package_name = package_name
448
449     @staticmethod
450     def get_template_key(name):
451         return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
452
453     @staticmethod
454     def get_module_filename(name):
455         return ModuleLoader.get_template_key(name) + '.py'
456
457     @internalcode
458     def load(self, environment, name, globals=None):
459         key = self.get_template_key(name)
460         module = '%s.%s' % (self.package_name, key)
461         mod = getattr(self.module, module, None)
462         if mod is None:
463             try:
464                 mod = __import__(module, None, None, ['root'])
465             except ImportError:
466                 raise TemplateNotFound(name)
467
468             # remove the entry from sys.modules, we only want the attribute
469             # on the module object we have stored on the loader.
470             sys.modules.pop(module, None)
471
472         return environment.template_class.from_module_dict(
473             environment, mod.__dict__, globals)