Moved tmbundle into tarfile (does not check out properly on windows)
[jinja2.git] / jinja2 / loaders.py
1 # -*- coding: utf-8 -*-
2 """
3     jinja2.loaders
4     ~~~~~~~~~~~~~~
5
6     Jinja loader classes.
7
8     :copyright: (c) 2009 by the Jinja Team.
9     :license: BSD, see LICENSE for more details.
10 """
11 from os import path
12 try:
13     from hashlib import sha1
14 except ImportError:
15     from sha import new as sha1
16 from jinja2.exceptions import TemplateNotFound
17 from jinja2.utils import LRUCache, open_if_exists, internalcode
18
19
20 def split_template_path(template):
21     """Split a path into segments and perform a sanity check.  If it detects
22     '..' in the path it will raise a `TemplateNotFound` error.
23     """
24     pieces = []
25     for piece in template.split('/'):
26         if path.sep in piece \
27            or (path.altsep and path.altsep in piece) or \
28            piece == path.pardir:
29             raise TemplateNotFound(template)
30         elif piece and piece != '.':
31             pieces.append(piece)
32     return pieces
33
34
35 class BaseLoader(object):
36     """Baseclass for all loaders.  Subclass this and override `get_source` to
37     implement a custom loading mechanism.  The environment provides a
38     `get_template` method that calls the loader's `load` method to get the
39     :class:`Template` object.
40
41     A very basic example for a loader that looks up templates on the file
42     system could look like this::
43
44         from jinja2 import BaseLoader, TemplateNotFound
45         from os.path import join, exists, getmtime
46
47         class MyLoader(BaseLoader):
48
49             def __init__(self, path):
50                 self.path = path
51
52             def get_source(self, environment, template):
53                 path = join(self.path, template)
54                 if not exists(path):
55                     raise TemplateNotFound(template)
56                 mtime = getmtime(path)
57                 with file(path) as f:
58                     source = f.read().decode('utf-8')
59                 return source, path, lambda: mtime == getmtime(path)
60     """
61
62     def get_source(self, environment, template):
63         """Get the template source, filename and reload helper for a template.
64         It's passed the environment and template name and has to return a
65         tuple in the form ``(source, filename, uptodate)`` or raise a
66         `TemplateNotFound` error if it can't locate the template.
67
68         The source part of the returned tuple must be the source of the
69         template as unicode string or a ASCII bytestring.  The filename should
70         be the name of the file on the filesystem if it was loaded from there,
71         otherwise `None`.  The filename is used by python for the tracebacks
72         if no loader extension is used.
73
74         The last item in the tuple is the `uptodate` function.  If auto
75         reloading is enabled it's always called to check if the template
76         changed.  No arguments are passed so the function must store the
77         old state somewhere (for example in a closure).  If it returns `False`
78         the template will be reloaded.
79         """
80         raise TemplateNotFound(template)
81
82     @internalcode
83     def load(self, environment, name, globals=None):
84         """Loads a template.  This method looks up the template in the cache
85         or loads one by calling :meth:`get_source`.  Subclasses should not
86         override this method as loaders working on collections of other
87         loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
88         will not call this method but `get_source` directly.
89         """
90         code = None
91         if globals is None:
92             globals = {}
93
94         # first we try to get the source for this template together
95         # with the filename and the uptodate function.
96         source, filename, uptodate = self.get_source(environment, name)
97
98         # try to load the code from the bytecode cache if there is a
99         # bytecode cache configured.
100         bcc = environment.bytecode_cache
101         if bcc is not None:
102             bucket = bcc.get_bucket(environment, name, filename, source)
103             code = bucket.code
104
105         # if we don't have code so far (not cached, no longer up to
106         # date) etc. we compile the template
107         if code is None:
108             code = environment.compile(source, name, filename)
109
110         # if the bytecode cache is available and the bucket doesn't
111         # have a code so far, we give the bucket the new code and put
112         # it back to the bytecode cache.
113         if bcc is not None and bucket.code is None:
114             bucket.code = code
115             bcc.set_bucket(bucket)
116
117         return environment.template_class.from_code(environment, code,
118                                                     globals, uptodate)
119
120
121 class FileSystemLoader(BaseLoader):
122     """Loads templates from the file system.  This loader can find templates
123     in folders on the file system and is the preferred way to load them.
124
125     The loader takes the path to the templates as string, or if multiple
126     locations are wanted a list of them which is then looked up in the
127     given order:
128
129     >>> loader = FileSystemLoader('/path/to/templates')
130     >>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
131
132     Per default the template encoding is ``'utf-8'`` which can be changed
133     by setting the `encoding` parameter to something else.
134     """
135
136     def __init__(self, searchpath, encoding='utf-8'):
137         if isinstance(searchpath, basestring):
138             searchpath = [searchpath]
139         self.searchpath = list(searchpath)
140         self.encoding = encoding
141
142     def get_source(self, environment, template):
143         pieces = split_template_path(template)
144         for searchpath in self.searchpath:
145             filename = path.join(searchpath, *pieces)
146             f = open_if_exists(filename)
147             if f is None:
148                 continue
149             try:
150                 contents = f.read().decode(self.encoding)
151             finally:
152                 f.close()
153
154             mtime = path.getmtime(filename)
155             def uptodate():
156                 try:
157                     return path.getmtime(filename) == mtime
158                 except OSError:
159                     return False
160             return contents, filename, uptodate
161         raise TemplateNotFound(template)
162
163
164 class PackageLoader(BaseLoader):
165     """Load templates from python eggs or packages.  It is constructed with
166     the name of the python package and the path to the templates in that
167     package:
168
169     >>> loader = PackageLoader('mypackage', 'views')
170
171     If the package path is not given, ``'templates'`` is assumed.
172
173     Per default the template encoding is ``'utf-8'`` which can be changed
174     by setting the `encoding` parameter to something else.  Due to the nature
175     of eggs it's only possible to reload templates if the package was loaded
176     from the file system and not a zip file.
177     """
178
179     def __init__(self, package_name, package_path='templates',
180                  encoding='utf-8'):
181         from pkg_resources import DefaultProvider, ResourceManager, \
182                                   get_provider
183         provider = get_provider(package_name)
184         self.encoding = encoding
185         self.manager = ResourceManager()
186         self.filesystem_bound = isinstance(provider, DefaultProvider)
187         self.provider = provider
188         self.package_path = package_path
189
190     def get_source(self, environment, template):
191         pieces = split_template_path(template)
192         p = '/'.join((self.package_path,) + tuple(pieces))
193         if not self.provider.has_resource(p):
194             raise TemplateNotFound(template)
195
196         filename = uptodate = None
197         if self.filesystem_bound:
198             filename = self.provider.get_resource_filename(self.manager, p)
199             mtime = path.getmtime(filename)
200             def uptodate():
201                 try:
202                     return path.getmtime(filename) == mtime
203                 except OSError:
204                     return False
205
206         source = self.provider.get_resource_string(self.manager, p)
207         return source.decode(self.encoding), filename, uptodate
208
209
210 class DictLoader(BaseLoader):
211     """Loads a template from a python dict.  It's passed a dict of unicode
212     strings bound to template names.  This loader is useful for unittesting:
213
214     >>> loader = DictLoader({'index.html': 'source here'})
215
216     Because auto reloading is rarely useful this is disabled per default.
217     """
218
219     def __init__(self, mapping):
220         self.mapping = mapping
221
222     def get_source(self, environment, template):
223         if template in self.mapping:
224             source = self.mapping[template]
225             return source, None, lambda: source != self.mapping.get(template)
226         raise TemplateNotFound(template)
227
228
229 class FunctionLoader(BaseLoader):
230     """A loader that is passed a function which does the loading.  The
231     function becomes the name of the template passed and has to return either
232     an unicode string with the template source, a tuple in the form ``(source,
233     filename, uptodatefunc)`` or `None` if the template does not exist.
234
235     >>> def load_template(name):
236     ...     if name == 'index.html'
237     ...         return '...'
238     ...
239     >>> loader = FunctionLoader(load_template)
240
241     The `uptodatefunc` is a function that is called if autoreload is enabled
242     and has to return `True` if the template is still up to date.  For more
243     details have a look at :meth:`BaseLoader.get_source` which has the same
244     return value.
245     """
246
247     def __init__(self, load_func):
248         self.load_func = load_func
249
250     def get_source(self, environment, template):
251         rv = self.load_func(template)
252         if rv is None:
253             raise TemplateNotFound(template)
254         elif isinstance(rv, basestring):
255             return rv, None, None
256         return rv
257
258
259 class PrefixLoader(BaseLoader):
260     """A loader that is passed a dict of loaders where each loader is bound
261     to a prefix.  The prefix is delimited from the template by a slash per
262     default, which can be changed by setting the `delimiter` argument to
263     something else.
264
265     >>> loader = PrefixLoader({
266     ...     'app1':     PackageLoader('mypackage.app1'),
267     ...     'app2':     PackageLoader('mypackage.app2')
268     ... })
269
270     By loading ``'app1/index.html'`` the file from the app1 package is loaded,
271     by loading ``'app2/index.html'`` the file from the second.
272     """
273
274     def __init__(self, mapping, delimiter='/'):
275         self.mapping = mapping
276         self.delimiter = delimiter
277
278     def get_source(self, environment, template):
279         try:
280             prefix, template = template.split(self.delimiter, 1)
281             loader = self.mapping[prefix]
282         except (ValueError, KeyError):
283             raise TemplateNotFound(template)
284         return loader.get_source(environment, template)
285
286
287 class ChoiceLoader(BaseLoader):
288     """This loader works like the `PrefixLoader` just that no prefix is
289     specified.  If a template could not be found by one loader the next one
290     is tried.
291
292     >>> loader = ChoiceLoader([
293     ...     FileSystemLoader('/path/to/user/templates'),
294     ...     PackageLoader('mypackage')
295     ... ])
296
297     This is useful if you want to allow users to override builtin templates
298     from a different location.
299     """
300
301     def __init__(self, loaders):
302         self.loaders = loaders
303
304     def get_source(self, environment, template):
305         for loader in self.loaders:
306             try:
307                 return loader.get_source(environment, template)
308             except TemplateNotFound:
309                 pass
310         raise TemplateNotFound(template)