exceptions.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # exceptions.py
  2. # Copyright (C) 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com
  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. """exception classes"""
  7. import traceback, sys, re
  8. from mako import util
  9. class MakoException(Exception):
  10. pass
  11. class RuntimeException(MakoException):
  12. pass
  13. def _format_filepos(lineno, pos, filename):
  14. if filename is None:
  15. return " at line: %d char: %d" % (lineno, pos)
  16. else:
  17. return " in file '%s' at line: %d char: %d" % (filename, lineno, pos)
  18. class CompileException(MakoException):
  19. def __init__(self, message, source, lineno, pos, filename):
  20. MakoException.__init__(self, message + _format_filepos(lineno, pos, filename))
  21. self.lineno =lineno
  22. self.pos = pos
  23. self.filename = filename
  24. self.source = source
  25. class SyntaxException(MakoException):
  26. def __init__(self, message, source, lineno, pos, filename):
  27. MakoException.__init__(self, message + _format_filepos(lineno, pos, filename))
  28. self.lineno =lineno
  29. self.pos = pos
  30. self.filename = filename
  31. self.source = source
  32. class UnsupportedError(MakoException):
  33. """raised when a retired feature is used."""
  34. class TemplateLookupException(MakoException):
  35. pass
  36. class TopLevelLookupException(TemplateLookupException):
  37. pass
  38. class RichTraceback(object):
  39. """pulls the current exception from the sys traceback and extracts
  40. Mako-specific template information.
  41. Usage:
  42. RichTraceback()
  43. Properties:
  44. error - the exception instance.
  45. message - the exception error message as unicode
  46. source - source code of the file where the error occured.
  47. if the error occured within a compiled template,
  48. this is the template source.
  49. lineno - line number where the error occured. if the error
  50. occured within a compiled template, the line number
  51. is adjusted to that of the template source
  52. records - a list of 8-tuples containing the original
  53. python traceback elements, plus the
  54. filename, line number, source line, and full template source
  55. for the traceline mapped back to its originating source
  56. template, if any for that traceline (else the fields are None).
  57. reverse_records - the list of records in reverse
  58. traceback - a list of 4-tuples, in the same format as a regular
  59. python traceback, with template-corresponding
  60. traceback records replacing the originals
  61. reverse_traceback - the traceback list in reverse
  62. """
  63. def __init__(self, error=None, traceback=None):
  64. self.source, self.lineno = "", 0
  65. if error is None or traceback is None:
  66. t, value, tback = sys.exc_info()
  67. if error is None:
  68. error = value or t
  69. if traceback is None:
  70. traceback = tback
  71. self.error = error
  72. self.records = self._init(traceback)
  73. if isinstance(self.error, (CompileException, SyntaxException)):
  74. import mako.template
  75. self.source = self.error.source
  76. self.lineno = self.error.lineno
  77. self._has_source = True
  78. self._init_message()
  79. @property
  80. def errorname(self):
  81. return util.exception_name(self.error)
  82. def _init_message(self):
  83. """Find a unicode representation of self.error"""
  84. try:
  85. self.message = unicode(self.error)
  86. except UnicodeError:
  87. try:
  88. self.message = str(self.error)
  89. except UnicodeEncodeError:
  90. # Fallback to args as neither unicode nor
  91. # str(Exception(u'\xe6')) work in Python < 2.6
  92. self.message = self.error.args[0]
  93. if not isinstance(self.message, unicode):
  94. self.message = unicode(self.message, 'ascii', 'replace')
  95. def _get_reformatted_records(self, records):
  96. for rec in records:
  97. if rec[6] is not None:
  98. yield (rec[4], rec[5], rec[2], rec[6])
  99. else:
  100. yield tuple(rec[0:4])
  101. @property
  102. def traceback(self):
  103. """return a list of 4-tuple traceback records (i.e. normal python
  104. format) with template-corresponding lines remapped to the originating
  105. template.
  106. """
  107. return list(self._get_reformatted_records(self.records))
  108. @property
  109. def reverse_records(self):
  110. return reversed(self.records)
  111. @property
  112. def reverse_traceback(self):
  113. """return the same data as traceback, except in reverse order.
  114. """
  115. return list(self._get_reformatted_records(self.reverse_records))
  116. def _init(self, trcback):
  117. """format a traceback from sys.exc_info() into 7-item tuples,
  118. containing the regular four traceback tuple items, plus the original
  119. template filename, the line number adjusted relative to the template
  120. source, and code line from that line number of the template."""
  121. import mako.template
  122. mods = {}
  123. rawrecords = traceback.extract_tb(trcback)
  124. new_trcback = []
  125. for filename, lineno, function, line in rawrecords:
  126. if not line:
  127. line = ''
  128. try:
  129. (line_map, template_lines) = mods[filename]
  130. except KeyError:
  131. try:
  132. info = mako.template._get_module_info(filename)
  133. module_source = info.code
  134. template_source = info.source
  135. template_filename = info.template_filename or filename
  136. except KeyError:
  137. # A normal .py file (not a Template)
  138. if not util.py3k:
  139. try:
  140. fp = open(filename, 'rb')
  141. encoding = util.parse_encoding(fp)
  142. fp.close()
  143. except IOError:
  144. encoding = None
  145. if encoding:
  146. line = line.decode(encoding)
  147. else:
  148. line = line.decode('ascii', 'replace')
  149. new_trcback.append((filename, lineno, function, line,
  150. None, None, None, None))
  151. continue
  152. template_ln = module_ln = 1
  153. line_map = {}
  154. for line in module_source.split("\n"):
  155. match = re.match(r'\s*# SOURCE LINE (\d+)', line)
  156. if match:
  157. template_ln = int(match.group(1))
  158. else:
  159. template_ln += 1
  160. module_ln += 1
  161. line_map[module_ln] = template_ln
  162. template_lines = [line for line in
  163. template_source.split("\n")]
  164. mods[filename] = (line_map, template_lines)
  165. template_ln = line_map[lineno]
  166. if template_ln <= len(template_lines):
  167. template_line = template_lines[template_ln - 1]
  168. else:
  169. template_line = None
  170. new_trcback.append((filename, lineno, function,
  171. line, template_filename, template_ln,
  172. template_line, template_source))
  173. if not self.source:
  174. for l in range(len(new_trcback)-1, 0, -1):
  175. if new_trcback[l][5]:
  176. self.source = new_trcback[l][7]
  177. self.lineno = new_trcback[l][5]
  178. break
  179. else:
  180. if new_trcback:
  181. try:
  182. # A normal .py file (not a Template)
  183. fp = open(new_trcback[-1][0], 'rb')
  184. encoding = util.parse_encoding(fp)
  185. fp.seek(0)
  186. self.source = fp.read()
  187. fp.close()
  188. if encoding:
  189. self.source = self.source.decode(encoding)
  190. except IOError:
  191. self.source = ''
  192. self.lineno = new_trcback[-1][1]
  193. return new_trcback
  194. def text_error_template(lookup=None):
  195. """Provides a template that renders a stack trace in a similar format to
  196. the Python interpreter, substituting source template filenames, line
  197. numbers and code for that of the originating source template, as
  198. applicable.
  199. """
  200. import mako.template
  201. return mako.template.Template(r"""
  202. <%page args="error=None, traceback=None"/>
  203. <%!
  204. from mako.exceptions import RichTraceback
  205. %>\
  206. <%
  207. tback = RichTraceback(error=error, traceback=traceback)
  208. %>\
  209. Traceback (most recent call last):
  210. % for (filename, lineno, function, line) in tback.traceback:
  211. File "${filename}", line ${lineno}, in ${function or '?'}
  212. ${line | trim}
  213. % endfor
  214. ${tback.errorname}: ${tback.message}
  215. """)
  216. def html_error_template():
  217. """Provides a template that renders a stack trace in an HTML format,
  218. providing an excerpt of code as well as substituting source template
  219. filenames, line numbers and code for that of the originating source
  220. template, as applicable.
  221. The template's default encoding_errors value is 'htmlentityreplace'. the
  222. template has two options. With the full option disabled, only a section of
  223. an HTML document is returned. with the css option disabled, the default
  224. stylesheet won't be included.
  225. """
  226. import mako.template
  227. return mako.template.Template(r"""
  228. <%!
  229. from mako.exceptions import RichTraceback
  230. %>
  231. <%page args="full=True, css=True, error=None, traceback=None"/>
  232. % if full:
  233. <html>
  234. <head>
  235. <title>Mako Runtime Error</title>
  236. % endif
  237. % if css:
  238. <style>
  239. body { font-family:verdana; margin:10px 30px 10px 30px;}
  240. .stacktrace { margin:5px 5px 5px 5px; }
  241. .highlight { padding:0px 10px 0px 10px; background-color:#9F9FDF; }
  242. .nonhighlight { padding:0px; background-color:#DFDFDF; }
  243. .sample { padding:10px; margin:10px 10px 10px 10px; font-family:monospace; }
  244. .sampleline { padding:0px 10px 0px 10px; }
  245. .sourceline { margin:5px 5px 10px 5px; font-family:monospace;}
  246. .location { font-size:80%; }
  247. </style>
  248. % endif
  249. % if full:
  250. </head>
  251. <body>
  252. % endif
  253. <h2>Error !</h2>
  254. <%
  255. tback = RichTraceback(error=error, traceback=traceback)
  256. src = tback.source
  257. line = tback.lineno
  258. if src:
  259. lines = src.split('\n')
  260. else:
  261. lines = None
  262. %>
  263. <h3>${tback.errorname}: ${tback.message}</h3>
  264. % if lines:
  265. <div class="sample">
  266. <div class="nonhighlight">
  267. % for index in range(max(0, line-4),min(len(lines), line+5)):
  268. % if index + 1 == line:
  269. <div class="highlight">${index + 1} ${lines[index] | h}</div>
  270. % else:
  271. <div class="sampleline">${index + 1} ${lines[index] | h}</div>
  272. % endif
  273. % endfor
  274. </div>
  275. </div>
  276. % endif
  277. <div class="stacktrace">
  278. % for (filename, lineno, function, line) in tback.reverse_traceback:
  279. <div class="location">${filename}, line ${lineno}:</div>
  280. <div class="sourceline">${line | h}</div>
  281. % endfor
  282. </div>
  283. % if full:
  284. </body>
  285. </html>
  286. % endif
  287. """, output_encoding=sys.getdefaultencoding(), encoding_errors='htmlentityreplace')