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