lookup.py 13 KB

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