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
Copyright (C) 2012-2017 Edgewall Software