1# mako/exceptions.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 7"""exception classes""" 8 9import sys 10import traceback 11 12from mako import compat 13from mako import util 14 15 16class MakoException(Exception): 17 pass 18 19 20class RuntimeException(MakoException): 21 pass 22 23 24def _format_filepos(lineno, pos, filename): 25 if filename is None: 26 return " at line: %d char: %d" % (lineno, pos) 27 else: 28 return " in file '%s' at line: %d char: %d" % (filename, lineno, pos) 29 30 31class CompileException(MakoException): 32 def __init__(self, message, source, lineno, pos, filename): 33 MakoException.__init__( 34 self, message + _format_filepos(lineno, pos, filename) 35 ) 36 self.lineno = lineno 37 self.pos = pos 38 self.filename = filename 39 self.source = source 40 41 42class SyntaxException(MakoException): 43 def __init__(self, message, source, lineno, pos, filename): 44 MakoException.__init__( 45 self, message + _format_filepos(lineno, pos, filename) 46 ) 47 self.lineno = lineno 48 self.pos = pos 49 self.filename = filename 50 self.source = source 51 52 53class UnsupportedError(MakoException): 54 55 """raised when a retired feature is used.""" 56 57 58class NameConflictError(MakoException): 59 60 """raised when a reserved word is used inappropriately""" 61 62 63class TemplateLookupException(MakoException): 64 pass 65 66 67class TopLevelLookupException(TemplateLookupException): 68 pass 69 70 71class RichTraceback: 72 73 """Pull the current exception from the ``sys`` traceback and extracts 74 Mako-specific template information. 75 76 See the usage examples in :ref:`handling_exceptions`. 77 78 """ 79 80 def __init__(self, error=None, traceback=None): 81 self.source, self.lineno = "", 0 82 83 if error is None or traceback is None: 84 t, value, tback = sys.exc_info() 85 86 if error is None: 87 error = value or t 88 89 if traceback is None: 90 traceback = tback 91 92 self.error = error 93 self.records = self._init(traceback) 94 95 if isinstance(self.error, (CompileException, SyntaxException)): 96 self.source = self.error.source 97 self.lineno = self.error.lineno 98 self._has_source = True 99 100 self._init_message() 101 102 @property 103 def errorname(self): 104 return compat.exception_name(self.error) 105 106 def _init_message(self): 107 """Find a unicode representation of self.error""" 108 try: 109 self.message = str(self.error) 110 except UnicodeError: 111 try: 112 self.message = str(self.error) 113 except UnicodeEncodeError: 114 # Fallback to args as neither unicode nor 115 # str(Exception(u'\xe6')) work in Python < 2.6 116 self.message = self.error.args[0] 117 if not isinstance(self.message, str): 118 self.message = str(self.message, "ascii", "replace") 119 120 def _get_reformatted_records(self, records): 121 for rec in records: 122 if rec[6] is not None: 123 yield (rec[4], rec[5], rec[2], rec[6]) 124 else: 125 yield tuple(rec[0:4]) 126 127 @property 128 def traceback(self): 129 """Return a list of 4-tuple traceback records (i.e. normal python 130 format) with template-corresponding lines remapped to the originating 131 template. 132 133 """ 134 return list(self._get_reformatted_records(self.records)) 135 136 @property 137 def reverse_records(self): 138 return reversed(self.records) 139 140 @property 141 def reverse_traceback(self): 142 """Return the same data as traceback, except in reverse order.""" 143 144 return list(self._get_reformatted_records(self.reverse_records)) 145 146 def _init(self, trcback): 147 """format a traceback from sys.exc_info() into 7-item tuples, 148 containing the regular four traceback tuple items, plus the original 149 template filename, the line number adjusted relative to the template 150 source, and code line from that line number of the template.""" 151 152 import mako.template 153 154 mods = {} 155 rawrecords = traceback.extract_tb(trcback) 156 new_trcback = [] 157 for filename, lineno, function, line in rawrecords: 158 if not line: 159 line = "" 160 try: 161 (line_map, template_lines, template_filename) = mods[filename] 162 except KeyError: 163 try: 164 info = mako.template._get_module_info(filename) 165 module_source = info.code 166 template_source = info.source 167 template_filename = ( 168 info.template_filename or info.template_uri or filename 169 ) 170 except KeyError: 171 # A normal .py file (not a Template) 172 new_trcback.append( 173 ( 174 filename, 175 lineno, 176 function, 177 line, 178 None, 179 None, 180 None, 181 None, 182 ) 183 ) 184 continue 185 186 template_ln = 1 187 188 mtm = mako.template.ModuleInfo 189 source_map = mtm.get_module_source_metadata( 190 module_source, full_line_map=True 191 ) 192 line_map = source_map["full_line_map"] 193 194 template_lines = [ 195 line_ for line_ in template_source.split("\n") 196 ] 197 mods[filename] = (line_map, template_lines, template_filename) 198 199 template_ln = line_map[lineno - 1] 200 201 if template_ln <= len(template_lines): 202 template_line = template_lines[template_ln - 1] 203 else: 204 template_line = None 205 new_trcback.append( 206 ( 207 filename, 208 lineno, 209 function, 210 line, 211 template_filename, 212 template_ln, 213 template_line, 214 template_source, 215 ) 216 ) 217 if not self.source: 218 for l in range(len(new_trcback) - 1, 0, -1): 219 if new_trcback[l][5]: 220 self.source = new_trcback[l][7] 221 self.lineno = new_trcback[l][5] 222 break 223 else: 224 if new_trcback: 225 try: 226 # A normal .py file (not a Template) 227 with open(new_trcback[-1][0], "rb") as fp: 228 encoding = util.parse_encoding(fp) 229 if not encoding: 230 encoding = "utf-8" 231 fp.seek(0) 232 self.source = fp.read() 233 if encoding: 234 self.source = self.source.decode(encoding) 235 except IOError: 236 self.source = "" 237 self.lineno = new_trcback[-1][1] 238 return new_trcback 239 240 241def text_error_template(lookup=None): 242 """Provides a template that renders a stack trace in a similar format to 243 the Python interpreter, substituting source template filenames, line 244 numbers and code for that of the originating source template, as 245 applicable. 246 247 """ 248 import mako.template 249 250 return mako.template.Template( 251 r""" 252<%page args="error=None, traceback=None"/> 253<%! 254 from mako.exceptions import RichTraceback 255%>\ 256<% 257 tback = RichTraceback(error=error, traceback=traceback) 258%>\ 259Traceback (most recent call last): 260% for (filename, lineno, function, line) in tback.traceback: 261 File "${filename}", line ${lineno}, in ${function or '?'} 262 ${line | trim} 263% endfor 264${tback.errorname}: ${tback.message} 265""" 266 ) 267 268 269def _install_pygments(): 270 global syntax_highlight, pygments_html_formatter 271 from mako.ext.pygmentplugin import syntax_highlight # noqa 272 from mako.ext.pygmentplugin import pygments_html_formatter # noqa 273 274 275def _install_fallback(): 276 global syntax_highlight, pygments_html_formatter 277 from mako.filters import html_escape 278 279 pygments_html_formatter = None 280 281 def syntax_highlight(filename="", language=None): 282 return html_escape 283 284 285def _install_highlighting(): 286 try: 287 _install_pygments() 288 except ImportError: 289 _install_fallback() 290 291 292_install_highlighting() 293 294 295def html_error_template(): 296 """Provides a template that renders a stack trace in an HTML format, 297 providing an excerpt of code as well as substituting source template 298 filenames, line numbers and code for that of the originating source 299 template, as applicable. 300 301 The template's default ``encoding_errors`` value is 302 ``'htmlentityreplace'``. The template has two options. With the 303 ``full`` option disabled, only a section of an HTML document is 304 returned. With the ``css`` option disabled, the default stylesheet 305 won't be included. 306 307 """ 308 import mako.template 309 310 return mako.template.Template( 311 r""" 312<%! 313 from mako.exceptions import RichTraceback, syntax_highlight,\ 314 pygments_html_formatter 315%> 316<%page args="full=True, css=True, error=None, traceback=None"/> 317% if full: 318<html> 319<head> 320 <title>Mako Runtime Error</title> 321% endif 322% if css: 323 <style> 324 body { font-family:verdana; margin:10px 30px 10px 30px;} 325 .stacktrace { margin:5px 5px 5px 5px; } 326 .highlight { padding:0px 10px 0px 10px; background-color:#9F9FDF; } 327 .nonhighlight { padding:0px; background-color:#DFDFDF; } 328 .sample { padding:10px; margin:10px 10px 10px 10px; 329 font-family:monospace; } 330 .sampleline { padding:0px 10px 0px 10px; } 331 .sourceline { margin:5px 5px 10px 5px; font-family:monospace;} 332 .location { font-size:80%; } 333 .highlight { white-space:pre; } 334 .sampleline { white-space:pre; } 335 336 % if pygments_html_formatter: 337 ${pygments_html_formatter.get_style_defs()} 338 .linenos { min-width: 2.5em; text-align: right; } 339 pre { margin: 0; } 340 .syntax-highlighted { padding: 0 10px; } 341 .syntax-highlightedtable { border-spacing: 1px; } 342 .nonhighlight { border-top: 1px solid #DFDFDF; 343 border-bottom: 1px solid #DFDFDF; } 344 .stacktrace .nonhighlight { margin: 5px 15px 10px; } 345 .sourceline { margin: 0 0; font-family:monospace; } 346 .code { background-color: #F8F8F8; width: 100%; } 347 .error .code { background-color: #FFBDBD; } 348 .error .syntax-highlighted { background-color: #FFBDBD; } 349 % endif 350 351 </style> 352% endif 353% if full: 354</head> 355<body> 356% endif 357 358<h2>Error !</h2> 359<% 360 tback = RichTraceback(error=error, traceback=traceback) 361 src = tback.source 362 line = tback.lineno 363 if src: 364 lines = src.split('\n') 365 else: 366 lines = None 367%> 368<h3>${tback.errorname}: ${tback.message|h}</h3> 369 370% if lines: 371 <div class="sample"> 372 <div class="nonhighlight"> 373% for index in range(max(0, line-4),min(len(lines), line+5)): 374 <% 375 if pygments_html_formatter: 376 pygments_html_formatter.linenostart = index + 1 377 %> 378 % if index + 1 == line: 379 <% 380 if pygments_html_formatter: 381 old_cssclass = pygments_html_formatter.cssclass 382 pygments_html_formatter.cssclass = 'error ' + old_cssclass 383 %> 384 ${lines[index] | syntax_highlight(language='mako')} 385 <% 386 if pygments_html_formatter: 387 pygments_html_formatter.cssclass = old_cssclass 388 %> 389 % else: 390 ${lines[index] | syntax_highlight(language='mako')} 391 % endif 392% endfor 393 </div> 394 </div> 395% endif 396 397<div class="stacktrace"> 398% for (filename, lineno, function, line) in tback.reverse_traceback: 399 <div class="location">${filename}, line ${lineno}:</div> 400 <div class="nonhighlight"> 401 <% 402 if pygments_html_formatter: 403 pygments_html_formatter.linenostart = lineno 404 %> 405 <div class="sourceline">${line | syntax_highlight(filename)}</div> 406 </div> 407% endfor 408</div> 409 410% if full: 411</body> 412</html> 413% endif 414""", 415 output_encoding=sys.getdefaultencoding(), 416 encoding_errors="htmlentityreplace", 417 ) 418