Mercurial > genshi > genshi-test
comparison genshi/template/loader.py @ 820:1837f39efd6f experimental-inline
Sync (old) experimental inline branch with trunk@1027.
author | cmlenz |
---|---|
date | Wed, 11 Mar 2009 17:51:06 +0000 |
parents | 0742f421caba |
children | de82830f8816 |
comparison
equal
deleted
inserted
replaced
500:0742f421caba | 820:1837f39efd6f |
---|---|
1 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- |
2 # | 2 # |
3 # Copyright (C) 2006-2007 Edgewall Software | 3 # Copyright (C) 2006-2008 Edgewall Software |
4 # All rights reserved. | 4 # All rights reserved. |
5 # | 5 # |
6 # This software is licensed as described in the file COPYING, which | 6 # This software is licensed as described in the file COPYING, which |
7 # you should have received as part of this distribution. The terms | 7 # you should have received as part of this distribution. The terms |
8 # are also available at http://genshi.edgewall.org/wiki/License. | 8 # are also available at http://genshi.edgewall.org/wiki/License. |
20 import dummy_threading as threading | 20 import dummy_threading as threading |
21 | 21 |
22 from genshi.template.base import TemplateError | 22 from genshi.template.base import TemplateError |
23 from genshi.util import LRUCache | 23 from genshi.util import LRUCache |
24 | 24 |
25 __all__ = ['TemplateLoader', 'TemplateNotFound'] | 25 __all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package', |
26 'prefixed'] | |
26 __docformat__ = 'restructuredtext en' | 27 __docformat__ = 'restructuredtext en' |
27 | 28 |
28 | 29 |
29 class TemplateNotFound(TemplateError): | 30 class TemplateNotFound(TemplateError): |
30 """Exception raised when a specific template file could not be found.""" | 31 """Exception raised when a specific template file could not be found.""" |
67 results in the same instance being returned: | 68 results in the same instance being returned: |
68 | 69 |
69 >>> loader.load(os.path.basename(path)) is template | 70 >>> loader.load(os.path.basename(path)) is template |
70 True | 71 True |
71 | 72 |
73 The `auto_reload` option can be used to control whether a template should | |
74 be automatically reloaded when the file it was loaded from has been | |
75 changed. Disable this automatic reloading to improve performance. | |
76 | |
72 >>> os.remove(path) | 77 >>> os.remove(path) |
73 """ | 78 """ |
74 def __init__(self, search_path=None, auto_reload=False, | 79 def __init__(self, search_path=None, auto_reload=False, |
75 default_encoding=None, max_cache_size=25, default_class=None, | 80 default_encoding=None, max_cache_size=25, default_class=None, |
76 variable_lookup='lenient', callback=None): | 81 variable_lookup='strict', allow_exec=True, callback=None): |
77 """Create the template laoder. | 82 """Create the template laoder. |
78 | 83 |
79 :param search_path: a list of absolute path names that should be | 84 :param search_path: a list of absolute path names that should be |
80 searched for template files, or a string containing | 85 searched for template files, or a string containing |
81 a single absolute path | 86 a single absolute path; alternatively, any item on |
87 the list may be a ''load function'' that is passed | |
88 a filename and returns a file-like object and some | |
89 metadata | |
82 :param auto_reload: whether to check the last modification time of | 90 :param auto_reload: whether to check the last modification time of |
83 template files, and reload them if they have changed | 91 template files, and reload them if they have changed |
84 :param default_encoding: the default encoding to assume when loading | 92 :param default_encoding: the default encoding to assume when loading |
85 templates; defaults to UTF-8 | 93 templates; defaults to UTF-8 |
86 :param max_cache_size: the maximum number of templates to keep in the | 94 :param max_cache_size: the maximum number of templates to keep in the |
87 cache | 95 cache |
88 :param default_class: the default `Template` subclass to use when | 96 :param default_class: the default `Template` subclass to use when |
89 instantiating templates | 97 instantiating templates |
90 :param variable_lookup: the variable lookup mechanism; either "lenient" | 98 :param variable_lookup: the variable lookup mechanism; either "strict" |
91 (the default), "strict", or a custom lookup | 99 (the default), "lenient", or a custom lookup |
92 class | 100 class |
101 :param allow_exec: whether to allow Python code blocks in templates | |
93 :param callback: (optional) a callback function that is invoked after a | 102 :param callback: (optional) a callback function that is invoked after a |
94 template was initialized by this loader; the function | 103 template was initialized by this loader; the function |
95 is passed the template object as only argument. This | 104 is passed the template object as only argument. This |
96 callback can be used for example to add any desired | 105 callback can be used for example to add any desired |
97 filters to the template | 106 filters to the template |
98 :see: `LenientLookup`, `StrictLookup` | 107 :see: `LenientLookup`, `StrictLookup` |
108 | |
109 :note: Changed in 0.5: Added the `allow_exec` argument | |
99 """ | 110 """ |
100 from genshi.template.markup import MarkupTemplate | 111 from genshi.template.markup import MarkupTemplate |
101 | 112 |
102 self.search_path = search_path | 113 self.search_path = search_path |
103 if self.search_path is None: | 114 if self.search_path is None: |
104 self.search_path = [] | 115 self.search_path = [] |
105 elif isinstance(self.search_path, basestring): | 116 elif not isinstance(self.search_path, (list, tuple)): |
106 self.search_path = [self.search_path] | 117 self.search_path = [self.search_path] |
118 | |
107 self.auto_reload = auto_reload | 119 self.auto_reload = auto_reload |
120 """Whether templates should be reloaded when the underlying file is | |
121 changed""" | |
122 | |
108 self.default_encoding = default_encoding | 123 self.default_encoding = default_encoding |
109 self.default_class = default_class or MarkupTemplate | 124 self.default_class = default_class or MarkupTemplate |
110 self.variable_lookup = variable_lookup | 125 self.variable_lookup = variable_lookup |
111 if callback is not None and not callable(callback): | 126 self.allow_exec = allow_exec |
127 if callback is not None and not hasattr(callback, '__call__'): | |
112 raise TypeError('The "callback" parameter needs to be callable') | 128 raise TypeError('The "callback" parameter needs to be callable') |
113 self.callback = callback | 129 self.callback = callback |
114 self._cache = LRUCache(max_cache_size) | 130 self._cache = LRUCache(max_cache_size) |
115 self._mtime = {} | 131 self._uptodate = {} |
116 self._lock = threading.Lock() | 132 self._lock = threading.RLock() |
117 | 133 |
118 def load(self, filename, relative_to=None, cls=None, encoding=None): | 134 def load(self, filename, relative_to=None, cls=None, encoding=None): |
119 """Load the template with the given name. | 135 """Load the template with the given name. |
120 | 136 |
121 If the `filename` parameter is relative, this method searches the search | 137 If the `filename` parameter is relative, this method searches the |
122 path trying to locate a template matching the given name. If the file | 138 search path trying to locate a template matching the given name. If the |
123 name is an absolute path, the search path is ignored. | 139 file name is an absolute path, the search path is ignored. |
124 | 140 |
125 If the requested template is not found, a `TemplateNotFound` exception | 141 If the requested template is not found, a `TemplateNotFound` exception |
126 is raised. Otherwise, a `Template` object is returned that represents | 142 is raised. Otherwise, a `Template` object is returned that represents |
127 the parsed template. | 143 the parsed template. |
128 | 144 |
141 template is being loaded directly | 157 template is being loaded directly |
142 :param cls: the class of the template object to instantiate | 158 :param cls: the class of the template object to instantiate |
143 :param encoding: the encoding of the template to load; defaults to the | 159 :param encoding: the encoding of the template to load; defaults to the |
144 ``default_encoding`` of the loader instance | 160 ``default_encoding`` of the loader instance |
145 :return: the loaded `Template` instance | 161 :return: the loaded `Template` instance |
146 :raises TemplateNotFound: if a template with the given name could not be | 162 :raises TemplateNotFound: if a template with the given name could not |
147 found | 163 be found |
148 """ | 164 """ |
149 if cls is None: | 165 if cls is None: |
150 cls = self.default_class | 166 cls = self.default_class |
151 if encoding is None: | 167 search_path = self.search_path |
152 encoding = self.default_encoding | 168 |
153 if relative_to and not os.path.isabs(relative_to): | 169 # Make the filename relative to the template file its being loaded |
170 # from, but only if that file is specified as a relative path, or no | |
171 # search path has been set up | |
172 if relative_to and (not search_path or not os.path.isabs(relative_to)): | |
154 filename = os.path.join(os.path.dirname(relative_to), filename) | 173 filename = os.path.join(os.path.dirname(relative_to), filename) |
174 | |
155 filename = os.path.normpath(filename) | 175 filename = os.path.normpath(filename) |
176 cachekey = filename | |
156 | 177 |
157 self._lock.acquire() | 178 self._lock.acquire() |
158 try: | 179 try: |
159 # First check the cache to avoid reparsing the same file | 180 # First check the cache to avoid reparsing the same file |
160 try: | 181 try: |
161 tmpl = self._cache[filename] | 182 tmpl = self._cache[cachekey] |
162 if not self.auto_reload or \ | 183 if not self.auto_reload: |
163 os.path.getmtime(tmpl.filepath) == self._mtime[filename]: | |
164 return tmpl | 184 return tmpl |
165 except KeyError: | 185 uptodate = self._uptodate[cachekey] |
186 if uptodate is not None and uptodate(): | |
187 return tmpl | |
188 except (KeyError, OSError): | |
166 pass | 189 pass |
167 | 190 |
168 search_path = self.search_path | |
169 isabs = False | 191 isabs = False |
170 | 192 |
171 if os.path.isabs(filename): | 193 if os.path.isabs(filename): |
172 # Bypass the search path if the requested filename is absolute | 194 # Bypass the search path if the requested filename is absolute |
173 search_path = [os.path.dirname(filename)] | 195 search_path = [os.path.dirname(filename)] |
176 elif relative_to and os.path.isabs(relative_to): | 198 elif relative_to and os.path.isabs(relative_to): |
177 # Make sure that the directory containing the including | 199 # Make sure that the directory containing the including |
178 # template is on the search path | 200 # template is on the search path |
179 dirname = os.path.dirname(relative_to) | 201 dirname = os.path.dirname(relative_to) |
180 if dirname not in search_path: | 202 if dirname not in search_path: |
181 search_path = search_path + [dirname] | 203 search_path = list(search_path) + [dirname] |
182 isabs = True | 204 isabs = True |
183 | 205 |
184 elif not search_path: | 206 elif not search_path: |
185 # Uh oh, don't know where to look for the template | 207 # Uh oh, don't know where to look for the template |
186 raise TemplateError('Search path for templates not configured') | 208 raise TemplateError('Search path for templates not configured') |
187 | 209 |
188 for dirname in search_path: | 210 for loadfunc in search_path: |
189 filepath = os.path.join(dirname, filename) | 211 if isinstance(loadfunc, basestring): |
212 loadfunc = directory(loadfunc) | |
190 try: | 213 try: |
191 fileobj = open(filepath, 'U') | 214 filepath, filename, fileobj, uptodate = loadfunc(filename) |
215 except IOError: | |
216 continue | |
217 else: | |
192 try: | 218 try: |
193 if isabs: | 219 if isabs: |
194 # If the filename of either the included or the | 220 # If the filename of either the included or the |
195 # including template is absolute, make sure the | 221 # including template is absolute, make sure the |
196 # included template gets an absolute path, too, | 222 # included template gets an absolute path, too, |
197 # so that nested include work properly without a | 223 # so that nested includes work properly without a |
198 # search path | 224 # search path |
199 filename = os.path.join(dirname, filename) | 225 filename = filepath |
200 dirname = '' | 226 tmpl = self._instantiate(cls, fileobj, filepath, |
201 tmpl = cls(fileobj, basedir=dirname, filename=filename, | 227 filename, encoding=encoding) |
202 loader=self, lookup=self.variable_lookup, | |
203 encoding=encoding) | |
204 if self.callback: | 228 if self.callback: |
205 self.callback(tmpl) | 229 self.callback(tmpl) |
206 self._cache[filename] = tmpl | 230 self._cache[cachekey] = tmpl |
207 self._mtime[filename] = os.path.getmtime(filepath) | 231 self._uptodate[cachekey] = uptodate |
208 finally: | 232 finally: |
209 fileobj.close() | 233 if hasattr(fileobj, 'close'): |
234 fileobj.close() | |
210 return tmpl | 235 return tmpl |
211 except IOError: | |
212 continue | |
213 | 236 |
214 raise TemplateNotFound(filename, search_path) | 237 raise TemplateNotFound(filename, search_path) |
215 | 238 |
216 finally: | 239 finally: |
217 self._lock.release() | 240 self._lock.release() |
241 | |
242 def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): | |
243 """Instantiate and return the `Template` object based on the given | |
244 class and parameters. | |
245 | |
246 This function is intended for subclasses to override if they need to | |
247 implement special template instantiation logic. Code that just uses | |
248 the `TemplateLoader` should use the `load` method instead. | |
249 | |
250 :param cls: the class of the template object to instantiate | |
251 :param fileobj: a readable file-like object containing the template | |
252 source | |
253 :param filepath: the absolute path to the template file | |
254 :param filename: the path to the template file relative to the search | |
255 path | |
256 :param encoding: the encoding of the template to load; defaults to the | |
257 ``default_encoding`` of the loader instance | |
258 :return: the loaded `Template` instance | |
259 :rtype: `Template` | |
260 """ | |
261 if encoding is None: | |
262 encoding = self.default_encoding | |
263 return cls(fileobj, filepath=filepath, filename=filename, loader=self, | |
264 encoding=encoding, lookup=self.variable_lookup, | |
265 allow_exec=self.allow_exec) | |
266 | |
267 def directory(path): | |
268 """Loader factory for loading templates from a local directory. | |
269 | |
270 :param path: the path to the local directory containing the templates | |
271 :return: the loader function to load templates from the given directory | |
272 :rtype: ``function`` | |
273 """ | |
274 def _load_from_directory(filename): | |
275 filepath = os.path.join(path, filename) | |
276 fileobj = open(filepath, 'U') | |
277 mtime = os.path.getmtime(filepath) | |
278 def _uptodate(): | |
279 return mtime == os.path.getmtime(filepath) | |
280 return filepath, filename, fileobj, _uptodate | |
281 return _load_from_directory | |
282 directory = staticmethod(directory) | |
283 | |
284 def package(name, path): | |
285 """Loader factory for loading templates from egg package data. | |
286 | |
287 :param name: the name of the package containing the resources | |
288 :param path: the path inside the package data | |
289 :return: the loader function to load templates from the given package | |
290 :rtype: ``function`` | |
291 """ | |
292 from pkg_resources import resource_stream | |
293 def _load_from_package(filename): | |
294 filepath = os.path.join(path, filename) | |
295 return filepath, filename, resource_stream(name, filepath), None | |
296 return _load_from_package | |
297 package = staticmethod(package) | |
298 | |
299 def prefixed(**delegates): | |
300 """Factory for a load function that delegates to other loaders | |
301 depending on the prefix of the requested template path. | |
302 | |
303 The prefix is stripped from the filename when passing on the load | |
304 request to the delegate. | |
305 | |
306 >>> load = prefixed( | |
307 ... app1 = lambda filename: ('app1', filename, None, None), | |
308 ... app2 = lambda filename: ('app2', filename, None, None) | |
309 ... ) | |
310 >>> print load('app1/foo.html') | |
311 ('app1', 'app1/foo.html', None, None) | |
312 >>> print load('app2/bar.html') | |
313 ('app2', 'app2/bar.html', None, None) | |
314 | |
315 :param delegates: mapping of path prefixes to loader functions | |
316 :return: the loader function | |
317 :rtype: ``function`` | |
318 """ | |
319 def _dispatch_by_prefix(filename): | |
320 for prefix, delegate in delegates.items(): | |
321 if filename.startswith(prefix): | |
322 if isinstance(delegate, basestring): | |
323 delegate = directory(delegate) | |
324 filepath, _, fileobj, uptodate = delegate( | |
325 filename[len(prefix):].lstrip('/\\') | |
326 ) | |
327 return filepath, filename, fileobj, uptodate | |
328 raise TemplateNotFound(filename, delegates.keys()) | |
329 return _dispatch_by_prefix | |
330 prefixed = staticmethod(prefixed) | |
331 | |
332 directory = TemplateLoader.directory | |
333 package = TemplateLoader.package | |
334 prefixed = TemplateLoader.prefixed |