blob: cdda94abab6c6755d0dc2ad488e4191e9480ad60 [file] [log] [blame]
Armin Ronacher07bc6842008-03-31 14:18:49 +02001# -*- coding: utf-8 -*-
2"""
Armin Ronacherbcb7c532008-04-11 16:30:34 +02003 jinja2.loaders
4 ~~~~~~~~~~~~~~
Armin Ronacher07bc6842008-03-31 14:18:49 +02005
6 Jinja loader classes.
7
Armin Ronacher7259c762008-04-30 13:03:59 +02008 XXX: move caching from the loaders to environment.get_template and add
9 environment overlays that allow to redefine escaping and other things but
10 shared the globals and filter mappings.
11
Armin Ronacherbcb7c532008-04-11 16:30:34 +020012 :copyright: 2008 by Armin Ronacher.
Armin Ronacher07bc6842008-03-31 14:18:49 +020013 :license: BSD, see LICENSE for more details.
14"""
Armin Ronacherbcb7c532008-04-11 16:30:34 +020015from os import path
16from jinja2.exceptions import TemplateNotFound
Armin Ronacher814f6c22008-04-17 15:52:23 +020017from jinja2.utils import LRUCache
Armin Ronacherbcb7c532008-04-11 16:30:34 +020018
19
Armin Ronacher9a822052008-04-17 18:44:07 +020020def 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 != '.':
31 pieces.append(piece)
32 return pieces
33
34
Armin Ronacherbcb7c532008-04-11 16:30:34 +020035class BaseLoader(object):
Armin Ronacher203bfcb2008-04-24 21:54:44 +020036 """Baseclass for all loaders. Subclass this and override `get_source` to
Armin Ronacherd1342312008-04-28 12:20:12 +020037 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.
Armin Ronacher814f6c22008-04-17 15:52:23 +020040
Armin Ronacherd1342312008-04-28 12:20:12 +020041 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, cache_size=50, auto_reload=True):
50 BaseLoader.__init__(self, cache_size, auto_reload)
51 self.path = path
52
53 def get_source(self, environment, template):
54 path = join(self.path, template)
55 if not exists(path):
56 raise TemplateNotFound(template)
57 mtime = getmtime(path)
58 with file(path) as f:
59 source = f.read().decode('utf-8')
60 return source, path, lambda: mtime != getmtime(path)
Armin Ronacher814f6c22008-04-17 15:52:23 +020061 """
62
Armin Ronacherbcb7c532008-04-11 16:30:34 +020063 def get_source(self, environment, template):
Armin Ronacher814f6c22008-04-17 15:52:23 +020064 """Get the template source, filename and reload helper for a template.
65 It's passed the environment and template name and has to return a
66 tuple in the form ``(source, filename, uptodate)`` or raise a
67 `TemplateNotFound` error if it can't locate the template.
68
69 The source part of the returned tuple must be the source of the
70 template as unicode string or a ASCII bytestring. The filename should
71 be the name of the file on the filesystem if it was loaded from there,
72 otherwise `None`. The filename is used by python for the tracebacks
73 if no loader extension is used.
74
75 The last item in the tuple is the `uptodate` function. If auto
76 reloading is enabled it's always called to check if the template
77 changed. No arguments are passed so the function must store the
78 old state somewhere (for example in a closure). If it returns `False`
79 the template will be reloaded.
80 """
81 raise TemplateNotFound(template)
Armin Ronacherbcb7c532008-04-11 16:30:34 +020082
Armin Ronacherba3757b2008-04-16 19:43:16 +020083 def load(self, environment, name, globals=None):
Armin Ronacherd1342312008-04-28 12:20:12 +020084 """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.
Armin Ronacher814f6c22008-04-17 15:52:23 +020089 """
90 if globals is None:
91 globals = {}
Armin Ronacher814f6c22008-04-17 15:52:23 +020092 source, filename, uptodate = self.get_source(environment, name)
93 code = environment.compile(source, name, filename, globals)
Armin Ronacher7259c762008-04-30 13:03:59 +020094 return environment.template_class.from_code(environment, code,
95 globals, uptodate)
Armin Ronacherbcb7c532008-04-11 16:30:34 +020096
97
98class FileSystemLoader(BaseLoader):
Armin Ronacherd1342312008-04-28 12:20:12 +020099 """Loads templates from the file system. This loader can find templates
100 in folders on the file system and is the preferred way to load them.
101
102 The loader takes the path to the templates as string, or if multiple
103 locations are wanted a list of them which is then looked up in the
104 given order:
105
106 >>> loader = FileSystemLoader('/path/to/templates')
107 >>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
108
109 Per default the template encoding is ``'utf-8'`` which can be changed
110 by setting the `encoding` parameter to something else.
111 """
Armin Ronacherbcb7c532008-04-11 16:30:34 +0200112
Armin Ronacher7259c762008-04-30 13:03:59 +0200113 def __init__(self, searchpath, encoding='utf-8'):
Armin Ronacher814f6c22008-04-17 15:52:23 +0200114 if isinstance(searchpath, basestring):
115 searchpath = [searchpath]
Armin Ronacherd1342312008-04-28 12:20:12 +0200116 self.searchpath = list(searchpath)
Armin Ronacherbcb7c532008-04-11 16:30:34 +0200117 self.encoding = encoding
118
119 def get_source(self, environment, template):
Armin Ronacher9a822052008-04-17 18:44:07 +0200120 pieces = split_template_path(template)
Armin Ronacher814f6c22008-04-17 15:52:23 +0200121 for searchpath in self.searchpath:
122 filename = path.join(searchpath, *pieces)
Armin Ronacher9a822052008-04-17 18:44:07 +0200123 if not path.isfile(filename):
124 continue
125 f = file(filename)
126 try:
127 contents = f.read().decode(self.encoding)
128 finally:
129 f.close()
130 old = path.getmtime(filename)
131 return contents, filename, lambda: path.getmtime(filename) != old
Armin Ronacher814f6c22008-04-17 15:52:23 +0200132 raise TemplateNotFound(template)
Armin Ronacher41ef36f2008-04-11 19:55:08 +0200133
134
Armin Ronacher9a822052008-04-17 18:44:07 +0200135class PackageLoader(BaseLoader):
Armin Ronacherd1342312008-04-28 12:20:12 +0200136 """Load templates from python eggs or packages. It is constructed with
137 the name of the python package and the path to the templates in that
138 package:
Armin Ronacher9a822052008-04-17 18:44:07 +0200139
Armin Ronacherd1342312008-04-28 12:20:12 +0200140 >>> loader = PackageLoader('mypackage', 'views')
141
142 If the package path is not given, ``'templates'`` is assumed.
143
144 Per default the template encoding is ``'utf-8'`` which can be changed
145 by setting the `encoding` parameter to something else. Due to the nature
146 of eggs it's only possible to reload templates if the package was loaded
147 from the file system and not a zip file.
148 """
149
150 def __init__(self, package_name, package_path='templates',
Armin Ronacher7259c762008-04-30 13:03:59 +0200151 encoding='utf-8'):
Armin Ronacherd1342312008-04-28 12:20:12 +0200152 from pkg_resources import DefaultProvider, ResourceManager, get_provider
153 provider = get_provider(package_name)
154 self.encoding = encoding
155 self.manager = ResourceManager()
156 self.filesystem_bound = isinstance(provider, DefaultProvider)
157 self.provider = provider
Armin Ronacher9a822052008-04-17 18:44:07 +0200158 self.package_path = package_path
159
160 def get_source(self, environment, template):
Armin Ronacher963f97d2008-04-25 11:44:59 +0200161 pieces = split_template_path(template)
Armin Ronacherd1342312008-04-28 12:20:12 +0200162 p = '/'.join((self.package_path,) + tuple(pieces))
163 if not self.provider.has_resource(p):
Armin Ronacher9a822052008-04-17 18:44:07 +0200164 raise TemplateNotFound(template)
Armin Ronacherd1342312008-04-28 12:20:12 +0200165
166 filename = uptodate = None
167 if self.filesystem_bound:
168 filename = self.provider.get_resource_filename(self.manager, p)
169 mtime = path.getmtime(filename)
170 def uptodate():
171 return path.getmtime(filename) != mtime
172
173 source = self.provider.get_resource_string(self.manager, p)
174 return source.decode(self.encoding), filename, uptodate
Armin Ronacher9a822052008-04-17 18:44:07 +0200175
176
Armin Ronacher41ef36f2008-04-11 19:55:08 +0200177class DictLoader(BaseLoader):
Armin Ronacherd1342312008-04-28 12:20:12 +0200178 """Loads a template from a python dict. It's passed a dict of unicode
179 strings bound to template names. This loader is useful for unittesting:
Armin Ronacher41ef36f2008-04-11 19:55:08 +0200180
Armin Ronacherd1342312008-04-28 12:20:12 +0200181 >>> loader = DictLoader({'index.html': 'source here'})
182
183 Because auto reloading is rarely useful this is disabled per default.
184 """
185
Armin Ronacher7259c762008-04-30 13:03:59 +0200186 def __init__(self, mapping):
Armin Ronacher41ef36f2008-04-11 19:55:08 +0200187 self.mapping = mapping
188
189 def get_source(self, environment, template):
190 if template in self.mapping:
Armin Ronacherd1342312008-04-28 12:20:12 +0200191 source = self.mapping[template]
192 return source, None, lambda: source != self.mapping[template]
Armin Ronacher41ef36f2008-04-11 19:55:08 +0200193 raise TemplateNotFound(template)
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200194
195
196class FunctionLoader(BaseLoader):
197 """A loader that is passed a function which does the loading. The
Armin Ronacher963f97d2008-04-25 11:44:59 +0200198 function becomes the name of the template passed and has to return either
199 an unicode string with the template source, a tuple in the form ``(source,
200 filename, uptodatefunc)`` or `None` if the template does not exist.
Armin Ronacherd1342312008-04-28 12:20:12 +0200201
202 >>> def load_template(name):
203 ... if name == 'index.html'
204 ... return '...'
205 ...
206 >>> loader = FunctionLoader(load_template)
207
208 The `uptodatefunc` is a function that is called if autoreload is enabled
209 and has to return `True` if the template is still up to date. For more
210 details have a look at :meth:`BaseLoader.get_source` which has the same
211 return value.
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200212 """
213
Armin Ronacher7259c762008-04-30 13:03:59 +0200214 def __init__(self, load_func):
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200215 self.load_func = load_func
216
217 def get_source(self, environment, template):
Armin Ronacher963f97d2008-04-25 11:44:59 +0200218 rv = self.load_func(template)
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200219 if rv is None:
220 raise TemplateNotFound(template)
Armin Ronacher963f97d2008-04-25 11:44:59 +0200221 elif isinstance(rv, basestring):
222 return rv, None, None
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200223 return rv
224
225
226class PrefixLoader(BaseLoader):
227 """A loader that is passed a dict of loaders where each loader is bound
Armin Ronacher7259c762008-04-30 13:03:59 +0200228 to a prefix. The prefix is delimited from the template by a slash per
229 default, which can be changed by setting the `delimiter` argument to
230 something else.
Armin Ronacherd1342312008-04-28 12:20:12 +0200231
232 >>> loader = PrefixLoader({
233 ... 'app1': PackageLoader('mypackage.app1'),
234 ... 'app2': PackageLoader('mypackage.app2')
235 ... })
236
237 By loading ``'app1/index.html'`` the file from the app1 package is loaded,
238 by loading ``'app2/index.html'`` the file from the second.
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200239 """
240
Armin Ronacher7259c762008-04-30 13:03:59 +0200241 def __init__(self, mapping, delimiter='/'):
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200242 self.mapping = mapping
243 self.delimiter = delimiter
244
245 def get_source(self, environment, template):
246 try:
247 prefix, template = template.split(self.delimiter, 1)
248 loader = self.mapping[prefix]
249 except (ValueError, KeyError):
250 raise TemplateNotFound(template)
251 return loader.get_source(environment, template)
252
253
254class ChoiceLoader(BaseLoader):
255 """This loader works like the `PrefixLoader` just that no prefix is
256 specified. If a template could not be found by one loader the next one
Armin Ronacher7259c762008-04-30 13:03:59 +0200257 is tried.
Armin Ronacherd1342312008-04-28 12:20:12 +0200258
259 >>> loader = ChoiceLoader([
260 ... FileSystemLoader('/path/to/user/templates'),
261 ... PackageLoader('myapplication')
262 ])
263
264 This is useful if you want to allow users to override builtin templates
265 from a different location.
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200266 """
267
Armin Ronacher7259c762008-04-30 13:03:59 +0200268 def __init__(self, loaders):
Armin Ronacher203bfcb2008-04-24 21:54:44 +0200269 self.loaders = loaders
270
271 def get_source(self, environment, template):
272 for loader in self.loaders:
273 try:
274 return loader.get_source(environment, template)
275 except TemplateNotFound:
276 pass
277 raise TemplateNotFound(template)