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