1# mako/lookup.py 2# Copyright 2006-2023 the Mako authors and contributors <see AUTHORS file> 3# 4# This module is part of Mako and is released under 5# the MIT License: http://www.opensource.org/licenses/mit-license.php 6 7import os 8import posixpath 9import re 10import stat 11import threading 12 13from mako import exceptions 14from mako import util 15from mako.template import Template 16 17 18class TemplateCollection: 19 20 """Represent a collection of :class:`.Template` objects, 21 identifiable via URI. 22 23 A :class:`.TemplateCollection` is linked to the usage of 24 all template tags that address other templates, such 25 as ``<%include>``, ``<%namespace>``, and ``<%inherit>``. 26 The ``file`` attribute of each of those tags refers 27 to a string URI that is passed to that :class:`.Template` 28 object's :class:`.TemplateCollection` for resolution. 29 30 :class:`.TemplateCollection` is an abstract class, 31 with the usual default implementation being :class:`.TemplateLookup`. 32 33 """ 34 35 def has_template(self, uri): 36 """Return ``True`` if this :class:`.TemplateLookup` is 37 capable of returning a :class:`.Template` object for the 38 given ``uri``. 39 40 :param uri: String URI of the template to be resolved. 41 42 """ 43 try: 44 self.get_template(uri) 45 return True 46 except exceptions.TemplateLookupException: 47 return False 48 49 def get_template(self, uri, relativeto=None): 50 """Return a :class:`.Template` object corresponding to the given 51 ``uri``. 52 53 The default implementation raises 54 :class:`.NotImplementedError`. Implementations should 55 raise :class:`.TemplateLookupException` if the given ``uri`` 56 cannot be resolved. 57 58 :param uri: String URI of the template to be resolved. 59 :param relativeto: if present, the given ``uri`` is assumed to 60 be relative to this URI. 61 62 """ 63 raise NotImplementedError() 64 65 def filename_to_uri(self, uri, filename): 66 """Convert the given ``filename`` to a URI relative to 67 this :class:`.TemplateCollection`.""" 68 69 return uri 70 71 def adjust_uri(self, uri, filename): 72 """Adjust the given ``uri`` based on the calling ``filename``. 73 74 When this method is called from the runtime, the 75 ``filename`` parameter is taken directly to the ``filename`` 76 attribute of the calling template. Therefore a custom 77 :class:`.TemplateCollection` subclass can place any string 78 identifier desired in the ``filename`` parameter of the 79 :class:`.Template` objects it constructs and have them come back 80 here. 81 82 """ 83 return uri 84 85 86class TemplateLookup(TemplateCollection): 87 88 """Represent a collection of templates that locates template source files 89 from the local filesystem. 90 91 The primary argument is the ``directories`` argument, the list of 92 directories to search: 93 94 .. sourcecode:: python 95 96 lookup = TemplateLookup(["/path/to/templates"]) 97 some_template = lookup.get_template("/index.html") 98 99 The :class:`.TemplateLookup` can also be given :class:`.Template` objects 100 programatically using :meth:`.put_string` or :meth:`.put_template`: 101 102 .. sourcecode:: python 103 104 lookup = TemplateLookup() 105 lookup.put_string("base.html", ''' 106 <html><body>${self.next()}</body></html> 107 ''') 108 lookup.put_string("hello.html", ''' 109 <%include file='base.html'/> 110 111 Hello, world ! 112 ''') 113 114 115 :param directories: A list of directory names which will be 116 searched for a particular template URI. The URI is appended 117 to each directory and the filesystem checked. 118 119 :param collection_size: Approximate size of the collection used 120 to store templates. If left at its default of ``-1``, the size 121 is unbounded, and a plain Python dictionary is used to 122 relate URI strings to :class:`.Template` instances. 123 Otherwise, a least-recently-used cache object is used which 124 will maintain the size of the collection approximately to 125 the number given. 126 127 :param filesystem_checks: When at its default value of ``True``, 128 each call to :meth:`.TemplateLookup.get_template()` will 129 compare the filesystem last modified time to the time in 130 which an existing :class:`.Template` object was created. 131 This allows the :class:`.TemplateLookup` to regenerate a 132 new :class:`.Template` whenever the original source has 133 been updated. Set this to ``False`` for a very minor 134 performance increase. 135 136 :param modulename_callable: A callable which, when present, 137 is passed the path of the source file as well as the 138 requested URI, and then returns the full path of the 139 generated Python module file. This is used to inject 140 alternate schemes for Python module location. If left at 141 its default of ``None``, the built in system of generation 142 based on ``module_directory`` plus ``uri`` is used. 143 144 All other keyword parameters available for 145 :class:`.Template` are mirrored here. When new 146 :class:`.Template` objects are created, the keywords 147 established with this :class:`.TemplateLookup` are passed on 148 to each new :class:`.Template`. 149 150 """ 151 152 def __init__( 153 self, 154 directories=None, 155 module_directory=None, 156 filesystem_checks=True, 157 collection_size=-1, 158 format_exceptions=False, 159 error_handler=None, 160 output_encoding=None, 161 encoding_errors="strict", 162 cache_args=None, 163 cache_impl="beaker", 164 cache_enabled=True, 165 cache_type=None, 166 cache_dir=None, 167 cache_url=None, 168 modulename_callable=None, 169 module_writer=None, 170 default_filters=None, 171 buffer_filters=(), 172 strict_undefined=False, 173 imports=None, 174 future_imports=None, 175 enable_loop=True, 176 input_encoding=None, 177 preprocessor=None, 178 lexer_cls=None, 179 include_error_handler=None, 180 ): 181 self.directories = [ 182 posixpath.normpath(d) for d in util.to_list(directories, ()) 183 ] 184 self.module_directory = module_directory 185 self.modulename_callable = modulename_callable 186 self.filesystem_checks = filesystem_checks 187 self.collection_size = collection_size 188 189 if cache_args is None: 190 cache_args = {} 191 # transfer deprecated cache_* args 192 if cache_dir: 193 cache_args.setdefault("dir", cache_dir) 194 if cache_url: 195 cache_args.setdefault("url", cache_url) 196 if cache_type: 197 cache_args.setdefault("type", cache_type) 198 199 self.template_args = { 200 "format_exceptions": format_exceptions, 201 "error_handler": error_handler, 202 "include_error_handler": include_error_handler, 203 "output_encoding": output_encoding, 204 "cache_impl": cache_impl, 205 "encoding_errors": encoding_errors, 206 "input_encoding": input_encoding, 207 "module_directory": module_directory, 208 "module_writer": module_writer, 209 "cache_args": cache_args, 210 "cache_enabled": cache_enabled, 211 "default_filters": default_filters, 212 "buffer_filters": buffer_filters, 213 "strict_undefined": strict_undefined, 214 "imports": imports, 215 "future_imports": future_imports, 216 "enable_loop": enable_loop, 217 "preprocessor": preprocessor, 218 "lexer_cls": lexer_cls, 219 } 220 221 if collection_size == -1: 222 self._collection = {} 223 self._uri_cache = {} 224 else: 225 self._collection = util.LRUCache(collection_size) 226 self._uri_cache = util.LRUCache(collection_size) 227 self._mutex = threading.Lock() 228 229 def get_template(self, uri): 230 """Return a :class:`.Template` object corresponding to the given 231 ``uri``. 232 233 .. note:: The ``relativeto`` argument is not supported here at 234 the moment. 235 236 """ 237 238 try: 239 if self.filesystem_checks: 240 return self._check(uri, self._collection[uri]) 241 else: 242 return self._collection[uri] 243 except KeyError as e: 244 u = re.sub(r"^\/+", "", uri) 245 for dir_ in self.directories: 246 # make sure the path seperators are posix - os.altsep is empty 247 # on POSIX and cannot be used. 248 dir_ = dir_.replace(os.path.sep, posixpath.sep) 249 srcfile = posixpath.normpath(posixpath.join(dir_, u)) 250 if os.path.isfile(srcfile): 251 return self._load(srcfile, uri) 252 else: 253 raise exceptions.TopLevelLookupException( 254 "Can't locate template for uri %r" % uri 255 ) from e 256 257 def adjust_uri(self, uri, relativeto): 258 """Adjust the given ``uri`` based on the given relative URI.""" 259 260 key = (uri, relativeto) 261 if key in self._uri_cache: 262 return self._uri_cache[key] 263 264 if uri[0] == "/": 265 v = self._uri_cache[key] = uri 266 elif relativeto is not None: 267 v = self._uri_cache[key] = posixpath.join( 268 posixpath.dirname(relativeto), uri 269 ) 270 else: 271 v = self._uri_cache[key] = "/" + uri 272 return v 273 274 def filename_to_uri(self, filename): 275 """Convert the given ``filename`` to a URI relative to 276 this :class:`.TemplateCollection`.""" 277 278 try: 279 return self._uri_cache[filename] 280 except KeyError: 281 value = self._relativeize(filename) 282 self._uri_cache[filename] = value 283 return value 284 285 def _relativeize(self, filename): 286 """Return the portion of a filename that is 'relative' 287 to the directories in this lookup. 288 289 """ 290 291 filename = posixpath.normpath(filename) 292 for dir_ in self.directories: 293 if filename[0 : len(dir_)] == dir_: 294 return filename[len(dir_) :] 295 else: 296 return None 297 298 def _load(self, filename, uri): 299 self._mutex.acquire() 300 try: 301 try: 302 # try returning from collection one 303 # more time in case concurrent thread already loaded 304 return self._collection[uri] 305 except KeyError: 306 pass 307 try: 308 if self.modulename_callable is not None: 309 module_filename = self.modulename_callable(filename, uri) 310 else: 311 module_filename = None 312 self._collection[uri] = template = Template( 313 uri=uri, 314 filename=posixpath.normpath(filename), 315 lookup=self, 316 module_filename=module_filename, 317 **self.template_args, 318 ) 319 return template 320 except: 321 # if compilation fails etc, ensure 322 # template is removed from collection, 323 # re-raise 324 self._collection.pop(uri, None) 325 raise 326 finally: 327 self._mutex.release() 328 329 def _check(self, uri, template): 330 if template.filename is None: 331 return template 332 333 try: 334 template_stat = os.stat(template.filename) 335 if template.module._modified_time >= template_stat[stat.ST_MTIME]: 336 return template 337 self._collection.pop(uri, None) 338 return self._load(template.filename, uri) 339 except OSError as e: 340 self._collection.pop(uri, None) 341 raise exceptions.TemplateLookupException( 342 "Can't locate template for uri %r" % uri 343 ) from e 344 345 def put_string(self, uri, text): 346 """Place a new :class:`.Template` object into this 347 :class:`.TemplateLookup`, based on the given string of 348 ``text``. 349 350 """ 351 self._collection[uri] = Template( 352 text, lookup=self, uri=uri, **self.template_args 353 ) 354 355 def put_template(self, uri, template): 356 """Place a new :class:`.Template` object into this 357 :class:`.TemplateLookup`, based on the given 358 :class:`.Template` object. 359 360 """ 361 self._collection[uri] = template 362