Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/template/loader.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

131 statements  

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 

16try: 

17 from importlib.resources import open_binary as resources_open_binary 

18except ImportError: 

19 from importlib_resources import open_binary as resources_open_binary 

20import os 

21try: 

22 import threading 

23except ImportError: 

24 import dummy_threading as threading 

25 

26from genshi.compat import string_types 

27from genshi.template.base import TemplateError 

28from genshi.util import LRUCache 

29 

30__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package', 

31 'prefixed'] 

32__docformat__ = 'restructuredtext en' 

33 

34 

35class TemplateNotFound(TemplateError): 

36 """Exception raised when a specific template file could not be found.""" 

37 

38 def __init__(self, name, search_path): 

39 """Create the exception. 

40  

41 :param name: the filename of the template 

42 :param search_path: the search path used to lookup the template 

43 """ 

44 TemplateError.__init__(self, 'Template "%s" not found' % name) 

45 self.search_path = search_path 

46 

47 

48class TemplateLoader(object): 

49 """Responsible for loading templates from files on the specified search 

50 path. 

51  

52 >>> import tempfile 

53 >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') 

54 >>> os.write(fd, u'<p>$var</p>'.encode('utf-8')) 

55 11 

56 >>> os.close(fd) 

57  

58 The template loader accepts a list of directory paths that are then used 

59 when searching for template files, in the given order: 

60  

61 >>> loader = TemplateLoader([os.path.dirname(path)]) 

62  

63 The `load()` method first checks the template cache whether the requested 

64 template has already been loaded. If not, it attempts to locate the 

65 template file, and returns the corresponding `Template` object: 

66  

67 >>> from genshi.template import MarkupTemplate 

68 >>> template = loader.load(os.path.basename(path)) 

69 >>> isinstance(template, MarkupTemplate) 

70 True 

71  

72 Template instances are cached: requesting a template with the same name 

73 results in the same instance being returned: 

74  

75 >>> loader.load(os.path.basename(path)) is template 

76 True 

77  

78 The `auto_reload` option can be used to control whether a template should 

79 be automatically reloaded when the file it was loaded from has been 

80 changed. Disable this automatic reloading to improve performance. 

81  

82 >>> os.remove(path) 

83 """ 

84 def __init__(self, search_path=None, auto_reload=False, 

85 default_encoding=None, max_cache_size=25, default_class=None, 

86 variable_lookup='strict', allow_exec=True, callback=None): 

87 """Create the template laoder. 

88  

89 :param search_path: a list of absolute path names that should be 

90 searched for template files, or a string containing 

91 a single absolute path; alternatively, any item on 

92 the list may be a ''load function'' that is passed 

93 a filename and returns a file-like object and some 

94 metadata 

95 :param auto_reload: whether to check the last modification time of 

96 template files, and reload them if they have changed 

97 :param default_encoding: the default encoding to assume when loading 

98 templates; defaults to UTF-8 

99 :param max_cache_size: the maximum number of templates to keep in the 

100 cache 

101 :param default_class: the default `Template` subclass to use when 

102 instantiating templates 

103 :param variable_lookup: the variable lookup mechanism; either "strict" 

104 (the default), "lenient", or a custom lookup 

105 class 

106 :param allow_exec: whether to allow Python code blocks in templates 

107 :param callback: (optional) a callback function that is invoked after a 

108 template was initialized by this loader; the function 

109 is passed the template object as only argument. This 

110 callback can be used for example to add any desired 

111 filters to the template 

112 :see: `LenientLookup`, `StrictLookup` 

113  

114 :note: Changed in 0.5: Added the `allow_exec` argument 

115 """ 

116 from genshi.template.markup import MarkupTemplate 

117 

118 self.search_path = search_path 

119 if self.search_path is None: 

120 self.search_path = [] 

121 elif not isinstance(self.search_path, (list, tuple)): 

122 self.search_path = [self.search_path] 

123 

124 self.auto_reload = auto_reload 

125 """Whether templates should be reloaded when the underlying file is 

126 changed""" 

127 

128 self.default_encoding = default_encoding 

129 self.default_class = default_class or MarkupTemplate 

130 self.variable_lookup = variable_lookup 

131 self.allow_exec = allow_exec 

132 if callback is not None and not hasattr(callback, '__call__'): 

133 raise TypeError('The "callback" parameter needs to be callable') 

134 self.callback = callback 

135 self._cache = LRUCache(max_cache_size) 

136 self._uptodate = {} 

137 self._lock = threading.RLock() 

138 

139 def __getstate__(self): 

140 state = self.__dict__.copy() 

141 state['_lock'] = None 

142 return state 

143 

144 def __setstate__(self, state): 

145 self.__dict__ = state 

146 self._lock = threading.RLock() 

147 

148 def load(self, filename, relative_to=None, cls=None, encoding=None): 

149 """Load the template with the given name. 

150  

151 If the `filename` parameter is relative, this method searches the 

152 search path trying to locate a template matching the given name. If the 

153 file name is an absolute path, the search path is ignored. 

154  

155 If the requested template is not found, a `TemplateNotFound` exception 

156 is raised. Otherwise, a `Template` object is returned that represents 

157 the parsed template. 

158  

159 Template instances are cached to avoid having to parse the same 

160 template file more than once. Thus, subsequent calls of this method 

161 with the same template file name will return the same `Template` 

162 object (unless the ``auto_reload`` option is enabled and the file was 

163 changed since the last parse.) 

164  

165 If the `relative_to` parameter is provided, the `filename` is 

166 interpreted as being relative to that path. 

167  

168 :param filename: the relative path of the template file to load 

169 :param relative_to: the filename of the template from which the new 

170 template is being loaded, or ``None`` if the 

171 template is being loaded directly 

172 :param cls: the class of the template object to instantiate 

173 :param encoding: the encoding of the template to load; defaults to the 

174 ``default_encoding`` of the loader instance 

175 :return: the loaded `Template` instance 

176 :raises TemplateNotFound: if a template with the given name could not 

177 be found 

178 """ 

179 if cls is None: 

180 cls = self.default_class 

181 search_path = self.search_path 

182 

183 # Make the filename relative to the template file its being loaded 

184 # from, but only if that file is specified as a relative path, or no 

185 # search path has been set up 

186 if relative_to and (not search_path or not os.path.isabs(relative_to)): 

187 filename = os.path.join(os.path.dirname(relative_to), filename) 

188 

189 filename = os.path.normpath(filename) 

190 cachekey = filename 

191 

192 self._lock.acquire() 

193 try: 

194 # First check the cache to avoid reparsing the same file 

195 try: 

196 tmpl = self._cache[cachekey] 

197 if not self.auto_reload: 

198 return tmpl 

199 uptodate = self._uptodate[cachekey] 

200 if uptodate is not None and uptodate(): 

201 return tmpl 

202 except (KeyError, OSError): 

203 pass 

204 

205 isabs = False 

206 

207 if os.path.isabs(filename): 

208 # Bypass the search path if the requested filename is absolute 

209 search_path = [os.path.dirname(filename)] 

210 isabs = True 

211 

212 elif relative_to and os.path.isabs(relative_to): 

213 # Make sure that the directory containing the including 

214 # template is on the search path 

215 dirname = os.path.dirname(relative_to) 

216 if dirname not in search_path: 

217 search_path = list(search_path) + [dirname] 

218 isabs = True 

219 

220 elif not search_path: 

221 # Uh oh, don't know where to look for the template 

222 raise TemplateError('Search path for templates not configured') 

223 

224 for loadfunc in search_path: 

225 if isinstance(loadfunc, string_types): 

226 loadfunc = directory(loadfunc) 

227 try: 

228 filepath, filename, fileobj, uptodate = loadfunc(filename) 

229 except IOError: 

230 continue 

231 else: 

232 try: 

233 if isabs: 

234 # If the filename of either the included or the  

235 # including template is absolute, make sure the 

236 # included template gets an absolute path, too, 

237 # so that nested includes work properly without a 

238 # search path 

239 filename = filepath 

240 tmpl = self._instantiate(cls, fileobj, filepath, 

241 filename, encoding=encoding) 

242 if self.callback: 

243 self.callback(tmpl) 

244 self._cache[cachekey] = tmpl 

245 self._uptodate[cachekey] = uptodate 

246 finally: 

247 if hasattr(fileobj, 'close'): 

248 fileobj.close() 

249 return tmpl 

250 

251 raise TemplateNotFound(filename, search_path) 

252 

253 finally: 

254 self._lock.release() 

255 

256 def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): 

257 """Instantiate and return the `Template` object based on the given 

258 class and parameters. 

259  

260 This function is intended for subclasses to override if they need to 

261 implement special template instantiation logic. Code that just uses 

262 the `TemplateLoader` should use the `load` method instead. 

263  

264 :param cls: the class of the template object to instantiate 

265 :param fileobj: a readable file-like object containing the template 

266 source 

267 :param filepath: the absolute path to the template file 

268 :param filename: the path to the template file relative to the search 

269 path 

270 :param encoding: the encoding of the template to load; defaults to the 

271 ``default_encoding`` of the loader instance 

272 :return: the loaded `Template` instance 

273 :rtype: `Template` 

274 """ 

275 if encoding is None: 

276 encoding = self.default_encoding 

277 return cls(fileobj, filepath=filepath, filename=filename, loader=self, 

278 encoding=encoding, lookup=self.variable_lookup, 

279 allow_exec=self.allow_exec) 

280 

281 @staticmethod 

282 def directory(path): 

283 """Loader factory for loading templates from a local directory. 

284  

285 :param path: the path to the local directory containing the templates 

286 :return: the loader function to load templates from the given directory 

287 :rtype: ``function`` 

288 """ 

289 def _load_from_directory(filename): 

290 filepath = os.path.join(path, filename) 

291 fileobj = open(filepath, 'rb') 

292 mtime = os.path.getmtime(filepath) 

293 def _uptodate(): 

294 return mtime == os.path.getmtime(filepath) 

295 return filepath, filename, fileobj, _uptodate 

296 return _load_from_directory 

297 

298 @staticmethod 

299 def package(name, path): 

300 """Loader factory for loading templates from egg package data. 

301  

302 :param name: the name of the package containing the resources 

303 :param path: the path inside the package data 

304 :return: the loader function to load templates from the given package 

305 :rtype: ``function`` 

306 """ 

307 def _load_from_package(filename): 

308 filepath = os.path.join(path, filename) 

309 return ( 

310 filepath, 

311 filename, 

312 resources_open_binary(name, filepath), 

313 None, 

314 ) 

315 return _load_from_package 

316 

317 @staticmethod 

318 def prefixed(**delegates): 

319 """Factory for a load function that delegates to other loaders 

320 depending on the prefix of the requested template path. 

321  

322 The prefix is stripped from the filename when passing on the load 

323 request to the delegate. 

324  

325 >>> load = prefixed( 

326 ... app1 = lambda filename: ('app1', filename, None, None), 

327 ... app2 = lambda filename: ('app2', filename, None, None) 

328 ... ) 

329 >>> print(load('app1/foo.html')) 

330 ('app1', 'app1/foo.html', None, None) 

331 >>> print(load('app2/bar.html')) 

332 ('app2', 'app2/bar.html', None, None) 

333  

334 :param delegates: mapping of path prefixes to loader functions 

335 :return: the loader function 

336 :rtype: ``function`` 

337 """ 

338 def _dispatch_by_prefix(filename): 

339 for prefix, delegate in delegates.items(): 

340 if filename.startswith(prefix): 

341 if isinstance(delegate, string_types): 

342 delegate = directory(delegate) 

343 filepath, _, fileobj, uptodate = delegate( 

344 filename[len(prefix):].lstrip('/\\') 

345 ) 

346 return filepath, filename, fileobj, uptodate 

347 raise TemplateNotFound(filename, list(delegates.keys())) 

348 return _dispatch_by_prefix 

349 

350 

351directory = TemplateLoader.directory 

352package = TemplateLoader.package 

353prefixed = TemplateLoader.prefixed