ccd6ec183a2af664c4fc8a1e2fae7915892a7d9b
[jinja2.git] / jinja2 / loaders.py
1 # -*- coding: utf-8 -*-
2 """
3     jinja2.loaders
4     ~~~~~~~~~~~~~~
5
6     Jinja loader classes.
7
8     :copyright: 2008 by Armin Ronacher.
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
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     def load(self, environment, name, globals=None):
83         """Loads a template.  This method looks up the template in the cache
84         or loads one by calling :meth:`get_source`.  Subclasses should not
85         override this method as loaders working on collections of other
86         loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
87         will not call this method but `get_source` directly.
88         """
89         if globals is None:
90             globals = {}
91         source, filename, uptodate = self.get_source(environment, name)
92         code = environment.compile(source, name, filename)
93         return environment.template_class.from_code(environment, code,
94                                                     globals, uptodate)
95
96
97 class FileSystemLoader(BaseLoader):
98     """Loads templates from the file system.  This loader can find templates
99     in folders on the file system and is the preferred way to load them.
100
101     The loader takes the path to the templates as string, or if multiple
102     locations are wanted a list of them which is then looked up in the
103     given order:
104
105     >>> loader = FileSystemLoader('/path/to/templates')
106     >>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
107
108     Per default the template encoding is ``'utf-8'`` which can be changed
109     by setting the `encoding` parameter to something else.
110     """
111
112     def __init__(self, searchpath, encoding='utf-8'):
113         if isinstance(searchpath, basestring):
114             searchpath = [searchpath]
115         self.searchpath = list(searchpath)
116         self.encoding = encoding
117
118     def get_source(self, environment, template):
119         pieces = split_template_path(template)
120         for searchpath in self.searchpath:
121             filename = path.join(searchpath, *pieces)
122             if not path.isfile(filename):
123                 continue
124             f = file(filename)
125             try:
126                 contents = f.read().decode(self.encoding)
127             finally:
128                 f.close()
129             old = path.getmtime(filename)
130             return contents, filename, lambda: path.getmtime(filename) == old
131         raise TemplateNotFound(template)
132
133
134 class PackageLoader(BaseLoader):
135     """Load templates from python eggs or packages.  It is constructed with
136     the name of the python package and the path to the templates in that
137     package:
138
139     >>> loader = PackageLoader('mypackage', 'views')
140
141     If the package path is not given, ``'templates'`` is assumed.
142
143     Per default the template encoding is ``'utf-8'`` which can be changed
144     by setting the `encoding` parameter to something else.  Due to the nature
145     of eggs it's only possible to reload templates if the package was loaded
146     from the file system and not a zip file.
147     """
148
149     def __init__(self, package_name, package_path='templates',
150                  encoding='utf-8'):
151         from pkg_resources import DefaultProvider, ResourceManager, get_provider
152         provider = get_provider(package_name)
153         self.encoding = encoding
154         self.manager = ResourceManager()
155         self.filesystem_bound = isinstance(provider, DefaultProvider)
156         self.provider = provider
157         self.package_path = package_path
158
159     def get_source(self, environment, template):
160         pieces = split_template_path(template)
161         p = '/'.join((self.package_path,) + tuple(pieces))
162         if not self.provider.has_resource(p):
163             raise TemplateNotFound(template)
164
165         filename = uptodate = None
166         if self.filesystem_bound:
167             filename = self.provider.get_resource_filename(self.manager, p)
168             mtime = path.getmtime(filename)
169             def uptodate():
170                 return path.getmtime(filename) == mtime
171
172         source = self.provider.get_resource_string(self.manager, p)
173         return source.decode(self.encoding), filename, uptodate
174
175
176 class DictLoader(BaseLoader):
177     """Loads a template from a python dict.  It's passed a dict of unicode
178     strings bound to template names.  This loader is useful for unittesting:
179
180     >>> loader = DictLoader({'index.html': 'source here'})
181
182     Because auto reloading is rarely useful this is disabled per default.
183     """
184
185     def __init__(self, mapping):
186         self.mapping = mapping
187
188     def get_source(self, environment, template):
189         if template in self.mapping:
190             source = self.mapping[template]
191             return source, None, lambda: source != self.mapping[template]
192         raise TemplateNotFound(template)
193
194
195 class FunctionLoader(BaseLoader):
196     """A loader that is passed a function which does the loading.  The
197     function becomes the name of the template passed and has to return either
198     an unicode string with the template source, a tuple in the form ``(source,
199     filename, uptodatefunc)`` or `None` if the template does not exist.
200
201     >>> def load_template(name):
202     ...     if name == 'index.html'
203     ...         return '...'
204     ...
205     >>> loader = FunctionLoader(load_template)
206
207     The `uptodatefunc` is a function that is called if autoreload is enabled
208     and has to return `True` if the template is still up to date.  For more
209     details have a look at :meth:`BaseLoader.get_source` which has the same
210     return value.
211     """
212
213     def __init__(self, load_func):
214         self.load_func = load_func
215
216     def get_source(self, environment, template):
217         rv = self.load_func(template)
218         if rv is None:
219             raise TemplateNotFound(template)
220         elif isinstance(rv, basestring):
221             return rv, None, None
222         return rv
223
224
225 class PrefixLoader(BaseLoader):
226     """A loader that is passed a dict of loaders where each loader is bound
227     to a prefix.  The prefix is delimited from the template by a slash per
228     default, which can be changed by setting the `delimiter` argument to
229     something else.
230
231     >>> loader = PrefixLoader({
232     ...     'app1':     PackageLoader('mypackage.app1'),
233     ...     'app2':     PackageLoader('mypackage.app2')
234     ... })
235
236     By loading ``'app1/index.html'`` the file from the app1 package is loaded,
237     by loading ``'app2/index.html'`` the file from the second.
238     """
239
240     def __init__(self, mapping, delimiter='/'):
241         self.mapping = mapping
242         self.delimiter = delimiter
243
244     def get_source(self, environment, template):
245         try:
246             prefix, template = template.split(self.delimiter, 1)
247             loader = self.mapping[prefix]
248         except (ValueError, KeyError):
249             raise TemplateNotFound(template)
250         return loader.get_source(environment, template)
251
252
253 class ChoiceLoader(BaseLoader):
254     """This loader works like the `PrefixLoader` just that no prefix is
255     specified.  If a template could not be found by one loader the next one
256     is tried.
257
258     >>> loader = ChoiceLoader([
259     ...     FileSystemLoader('/path/to/user/templates'),
260     ...     PackageLoader('myapplication')
261     ... ])
262
263     This is useful if you want to allow users to override builtin templates
264     from a different location.
265     """
266
267     def __init__(self, loaders):
268         self.loaders = loaders
269
270     def get_source(self, environment, template):
271         for loader in self.loaders:
272             try:
273                 return loader.get_source(environment, template)
274             except TemplateNotFound:
275                 pass
276         raise TemplateNotFound(template)