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