1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4""" 5Formatters for the exception data that comes from ExceptionCollector. 6""" 7# @@: TODO: 8# Use this: http://www.zope.org/Members/tino/VisualTraceback/VisualTracebackNews 9 10import cgi 11import six 12import re 13from paste.util import PySourceColor 14 15def html_quote(s): 16 return cgi.escape(str(s), True) 17 18class AbstractFormatter(object): 19 20 general_data_order = ['object', 'source_url'] 21 22 def __init__(self, show_hidden_frames=False, 23 include_reusable=True, 24 show_extra_data=True, 25 trim_source_paths=()): 26 self.show_hidden_frames = show_hidden_frames 27 self.trim_source_paths = trim_source_paths 28 self.include_reusable = include_reusable 29 self.show_extra_data = show_extra_data 30 31 def format_collected_data(self, exc_data): 32 general_data = {} 33 if self.show_extra_data: 34 for name, value_list in exc_data.extra_data.items(): 35 if isinstance(name, tuple): 36 importance, title = name 37 else: 38 importance, title = 'normal', name 39 for value in value_list: 40 general_data[(importance, name)] = self.format_extra_data( 41 importance, title, value) 42 lines = [] 43 frames = self.filter_frames(exc_data.frames) 44 for frame in frames: 45 sup = frame.supplement 46 if sup: 47 if sup.object: 48 general_data[('important', 'object')] = self.format_sup_object( 49 sup.object) 50 if sup.source_url: 51 general_data[('important', 'source_url')] = self.format_sup_url( 52 sup.source_url) 53 if sup.line: 54 lines.append(self.format_sup_line_pos(sup.line, sup.column)) 55 if sup.expression: 56 lines.append(self.format_sup_expression(sup.expression)) 57 if sup.warnings: 58 for warning in sup.warnings: 59 lines.append(self.format_sup_warning(warning)) 60 if sup.info: 61 lines.extend(self.format_sup_info(sup.info)) 62 if frame.supplement_exception: 63 lines.append('Exception in supplement:') 64 lines.append(self.quote_long(frame.supplement_exception)) 65 if frame.traceback_info: 66 lines.append(self.format_traceback_info(frame.traceback_info)) 67 filename = frame.filename 68 if filename and self.trim_source_paths: 69 for path, repl in self.trim_source_paths: 70 if filename.startswith(path): 71 filename = repl + filename[len(path):] 72 break 73 lines.append(self.format_source_line(filename or '?', frame)) 74 source = frame.get_source_line() 75 long_source = frame.get_source_line(2) 76 if source: 77 lines.append(self.format_long_source( 78 source, long_source)) 79 etype = exc_data.exception_type 80 if not isinstance(etype, six.string_types): 81 etype = etype.__name__ 82 exc_info = self.format_exception_info( 83 etype, 84 exc_data.exception_value) 85 data_by_importance = {'important': [], 'normal': [], 86 'supplemental': [], 'extra': []} 87 for (importance, name), value in general_data.items(): 88 data_by_importance[importance].append( 89 (name, value)) 90 for value in data_by_importance.values(): 91 value.sort() 92 return self.format_combine(data_by_importance, lines, exc_info) 93 94 def filter_frames(self, frames): 95 """ 96 Removes any frames that should be hidden, according to the 97 values of traceback_hide, self.show_hidden_frames, and the 98 hidden status of the final frame. 99 """ 100 if self.show_hidden_frames: 101 return frames 102 new_frames = [] 103 hidden = False 104 for frame in frames: 105 hide = frame.traceback_hide 106 # @@: It would be nice to signal a warning if an unknown 107 # hide string was used, but I'm not sure where to put 108 # that warning. 109 if hide == 'before': 110 new_frames = [] 111 hidden = False 112 elif hide == 'before_and_this': 113 new_frames = [] 114 hidden = False 115 continue 116 elif hide == 'reset': 117 hidden = False 118 elif hide == 'reset_and_this': 119 hidden = False 120 continue 121 elif hide == 'after': 122 hidden = True 123 elif hide == 'after_and_this': 124 hidden = True 125 continue 126 elif hide: 127 continue 128 elif hidden: 129 continue 130 new_frames.append(frame) 131 if frames[-1] not in new_frames: 132 # We must include the last frame; that we don't indicates 133 # that the error happened where something was "hidden", 134 # so we just have to show everything 135 return frames 136 return new_frames 137 138 def pretty_string_repr(self, s): 139 """ 140 Formats the string as a triple-quoted string when it contains 141 newlines. 142 """ 143 if '\n' in s: 144 s = repr(s) 145 s = s[0]*3 + s[1:-1] + s[-1]*3 146 s = s.replace('\\n', '\n') 147 return s 148 else: 149 return repr(s) 150 151 def long_item_list(self, lst): 152 """ 153 Returns true if the list contains items that are long, and should 154 be more nicely formatted. 155 """ 156 how_many = 0 157 for item in lst: 158 if len(repr(item)) > 40: 159 how_many += 1 160 if how_many >= 3: 161 return True 162 return False 163 164class TextFormatter(AbstractFormatter): 165 166 def quote(self, s): 167 return s 168 def quote_long(self, s): 169 return s 170 def emphasize(self, s): 171 return s 172 def format_sup_object(self, obj): 173 return 'In object: %s' % self.emphasize(self.quote(repr(obj))) 174 def format_sup_url(self, url): 175 return 'URL: %s' % self.quote(url) 176 def format_sup_line_pos(self, line, column): 177 if column: 178 return self.emphasize('Line %i, Column %i' % (line, column)) 179 else: 180 return self.emphasize('Line %i' % line) 181 def format_sup_expression(self, expr): 182 return self.emphasize('In expression: %s' % self.quote(expr)) 183 def format_sup_warning(self, warning): 184 return 'Warning: %s' % self.quote(warning) 185 def format_sup_info(self, info): 186 return [self.quote_long(info)] 187 def format_source_line(self, filename, frame): 188 return 'File %r, line %s in %s' % ( 189 filename, frame.lineno or '?', frame.name or '?') 190 def format_long_source(self, source, long_source): 191 return self.format_source(source) 192 def format_source(self, source_line): 193 return ' ' + self.quote(source_line.strip()) 194 def format_exception_info(self, etype, evalue): 195 return self.emphasize( 196 '%s: %s' % (self.quote(etype), self.quote(evalue))) 197 def format_traceback_info(self, info): 198 return info 199 200 def format_combine(self, data_by_importance, lines, exc_info): 201 lines[:0] = [value for n, value in data_by_importance['important']] 202 lines.append(exc_info) 203 for name in 'normal', 'supplemental', 'extra': 204 lines.extend([value for n, value in data_by_importance[name]]) 205 return self.format_combine_lines(lines) 206 207 def format_combine_lines(self, lines): 208 return '\n'.join(lines) 209 210 def format_extra_data(self, importance, title, value): 211 if isinstance(value, str): 212 s = self.pretty_string_repr(value) 213 if '\n' in s: 214 return '%s:\n%s' % (title, s) 215 else: 216 return '%s: %s' % (title, s) 217 elif isinstance(value, dict): 218 lines = ['\n', title, '-'*len(title)] 219 items = value.items() 220 items.sort() 221 for n, v in items: 222 try: 223 v = repr(v) 224 except Exception as e: 225 v = 'Cannot display: %s' % e 226 v = truncate(v) 227 lines.append(' %s: %s' % (n, v)) 228 return '\n'.join(lines) 229 elif (isinstance(value, (list, tuple)) 230 and self.long_item_list(value)): 231 parts = [truncate(repr(v)) for v in value] 232 return '%s: [\n %s]' % ( 233 title, ',\n '.join(parts)) 234 else: 235 return '%s: %s' % (title, truncate(repr(value))) 236 237class HTMLFormatter(TextFormatter): 238 239 def quote(self, s): 240 return html_quote(s) 241 def quote_long(self, s): 242 return '<pre>%s</pre>' % self.quote(s) 243 def emphasize(self, s): 244 return '<b>%s</b>' % s 245 def format_sup_url(self, url): 246 return 'URL: <a href="%s">%s</a>' % (url, url) 247 def format_combine_lines(self, lines): 248 return '<br>\n'.join(lines) 249 def format_source_line(self, filename, frame): 250 name = self.quote(frame.name or '?') 251 return 'Module <span class="module" title="%s">%s</span>:<b>%s</b> in <code>%s</code>' % ( 252 filename, frame.modname or '?', frame.lineno or '?', 253 name) 254 return 'File %r, line %s in <tt>%s</tt>' % ( 255 filename, frame.lineno, name) 256 def format_long_source(self, source, long_source): 257 q_long_source = str2html(long_source, False, 4, True) 258 q_source = str2html(source, True, 0, False) 259 return ('<code style="display: none" class="source" source-type="long"><a class="switch_source" onclick="return switch_source(this, \'long\')" href="#"><< </a>%s</code>' 260 '<code class="source" source-type="short"><a onclick="return switch_source(this, \'short\')" class="switch_source" href="#">>> </a>%s</code>' 261 % (q_long_source, 262 q_source)) 263 def format_source(self, source_line): 264 return ' <code class="source">%s</code>' % self.quote(source_line.strip()) 265 def format_traceback_info(self, info): 266 return '<pre>%s</pre>' % self.quote(info) 267 268 def format_extra_data(self, importance, title, value): 269 if isinstance(value, str): 270 s = self.pretty_string_repr(value) 271 if '\n' in s: 272 return '%s:<br><pre>%s</pre>' % (title, self.quote(s)) 273 else: 274 return '%s: <tt>%s</tt>' % (title, self.quote(s)) 275 elif isinstance(value, dict): 276 return self.zebra_table(title, value) 277 elif (isinstance(value, (list, tuple)) 278 and self.long_item_list(value)): 279 return '%s: <tt>[<br>\n %s]</tt>' % ( 280 title, ',<br> '.join(map(self.quote, map(repr, value)))) 281 else: 282 return '%s: <tt>%s</tt>' % (title, self.quote(repr(value))) 283 284 def format_combine(self, data_by_importance, lines, exc_info): 285 lines[:0] = [value for n, value in data_by_importance['important']] 286 lines.append(exc_info) 287 for name in 'normal', 'supplemental': 288 lines.extend([value for n, value in data_by_importance[name]]) 289 if data_by_importance['extra']: 290 lines.append( 291 '<script type="text/javascript">\nshow_button(\'extra_data\', \'extra data\');\n</script>\n' + 292 '<div id="extra_data" class="hidden-data">\n') 293 lines.extend([value for n, value in data_by_importance['extra']]) 294 lines.append('</div>') 295 text = self.format_combine_lines(lines) 296 if self.include_reusable: 297 return error_css + hide_display_js + text 298 else: 299 # Usually because another error is already on this page, 300 # and so the js & CSS are unneeded 301 return text 302 303 def zebra_table(self, title, rows, table_class="variables"): 304 if isinstance(rows, dict): 305 rows = rows.items() 306 rows.sort() 307 table = ['<table class="%s">' % table_class, 308 '<tr class="header"><th colspan="2">%s</th></tr>' 309 % self.quote(title)] 310 odd = False 311 for name, value in rows: 312 try: 313 value = repr(value) 314 except Exception as e: 315 value = 'Cannot print: %s' % e 316 odd = not odd 317 table.append( 318 '<tr class="%s"><td>%s</td>' 319 % (odd and 'odd' or 'even', self.quote(name))) 320 table.append( 321 '<td><tt>%s</tt></td></tr>' 322 % make_wrappable(self.quote(truncate(value)))) 323 table.append('</table>') 324 return '\n'.join(table) 325 326hide_display_js = r''' 327<script type="text/javascript"> 328function hide_display(id) { 329 var el = document.getElementById(id); 330 if (el.className == "hidden-data") { 331 el.className = ""; 332 return true; 333 } else { 334 el.className = "hidden-data"; 335 return false; 336 } 337} 338document.write('<style type="text/css">\n'); 339document.write('.hidden-data {display: none}\n'); 340document.write('</style>\n'); 341function show_button(toggle_id, name) { 342 document.write('<a href="#' + toggle_id 343 + '" onclick="javascript:hide_display(\'' + toggle_id 344 + '\')" class="button">' + name + '</a><br>'); 345} 346 347function switch_source(el, hide_type) { 348 while (el) { 349 if (el.getAttribute && 350 el.getAttribute('source-type') == hide_type) { 351 break; 352 } 353 el = el.parentNode; 354 } 355 if (! el) { 356 return false; 357 } 358 el.style.display = 'none'; 359 if (hide_type == 'long') { 360 while (el) { 361 if (el.getAttribute && 362 el.getAttribute('source-type') == 'short') { 363 break; 364 } 365 el = el.nextSibling; 366 } 367 } else { 368 while (el) { 369 if (el.getAttribute && 370 el.getAttribute('source-type') == 'long') { 371 break; 372 } 373 el = el.previousSibling; 374 } 375 } 376 if (el) { 377 el.style.display = ''; 378 } 379 return false; 380} 381 382</script>''' 383 384 385error_css = """ 386<style type="text/css"> 387body { 388 font-family: Helvetica, sans-serif; 389} 390 391table { 392 width: 100%; 393} 394 395tr.header { 396 background-color: #006; 397 color: #fff; 398} 399 400tr.even { 401 background-color: #ddd; 402} 403 404table.variables td { 405 vertical-align: top; 406 overflow: auto; 407} 408 409a.button { 410 background-color: #ccc; 411 border: 2px outset #aaa; 412 color: #000; 413 text-decoration: none; 414} 415 416a.button:hover { 417 background-color: #ddd; 418} 419 420code.source { 421 color: #006; 422} 423 424a.switch_source { 425 color: #090; 426 text-decoration: none; 427} 428 429a.switch_source:hover { 430 background-color: #ddd; 431} 432 433.source-highlight { 434 background-color: #ff9; 435} 436 437</style> 438""" 439 440def format_html(exc_data, include_hidden_frames=False, **ops): 441 if not include_hidden_frames: 442 return HTMLFormatter(**ops).format_collected_data(exc_data) 443 short_er = format_html(exc_data, show_hidden_frames=False, **ops) 444 # @@: This should have a way of seeing if the previous traceback 445 # was actually trimmed at all 446 ops['include_reusable'] = False 447 ops['show_extra_data'] = False 448 long_er = format_html(exc_data, show_hidden_frames=True, **ops) 449 text_er = format_text(exc_data, show_hidden_frames=True, **ops) 450 return """ 451 %s 452 <br> 453 <script type="text/javascript"> 454 show_button('full_traceback', 'full traceback') 455 </script> 456 <div id="full_traceback" class="hidden-data"> 457 %s 458 </div> 459 <br> 460 <script type="text/javascript"> 461 show_button('text_version', 'text version') 462 </script> 463 <div id="text_version" class="hidden-data"> 464 <textarea style="width: 100%%" rows=10 cols=60>%s</textarea> 465 </div> 466 """ % (short_er, long_er, cgi.escape(text_er)) 467 468def format_text(exc_data, **ops): 469 return TextFormatter(**ops).format_collected_data(exc_data) 470 471whitespace_re = re.compile(r' +') 472pre_re = re.compile(r'</?pre.*?>') 473error_re = re.compile(r'<h3>ERROR: .*?</h3>') 474 475def str2html(src, strip=False, indent_subsequent=0, 476 highlight_inner=False): 477 """ 478 Convert a string to HTML. Try to be really safe about it, 479 returning a quoted version of the string if nothing else works. 480 """ 481 try: 482 return _str2html(src, strip=strip, 483 indent_subsequent=indent_subsequent, 484 highlight_inner=highlight_inner) 485 except: 486 return html_quote(src) 487 488def _str2html(src, strip=False, indent_subsequent=0, 489 highlight_inner=False): 490 if strip: 491 src = src.strip() 492 orig_src = src 493 try: 494 src = PySourceColor.str2html(src, form='snip') 495 src = error_re.sub('', src) 496 src = pre_re.sub('', src) 497 src = re.sub(r'^[\n\r]{0,1}', '', src) 498 src = re.sub(r'[\n\r]{0,1}$', '', src) 499 except: 500 src = html_quote(orig_src) 501 lines = src.splitlines() 502 if len(lines) == 1: 503 return lines[0] 504 indent = ' '*indent_subsequent 505 for i in range(1, len(lines)): 506 lines[i] = indent+lines[i] 507 if highlight_inner and i == len(lines)/2: 508 lines[i] = '<span class="source-highlight">%s</span>' % lines[i] 509 src = '<br>\n'.join(lines) 510 src = whitespace_re.sub( 511 lambda m: ' '*(len(m.group(0))-1) + ' ', src) 512 return src 513 514def truncate(string, limit=1000): 515 """ 516 Truncate the string to the limit number of 517 characters 518 """ 519 if len(string) > limit: 520 return string[:limit-20]+'...'+string[-17:] 521 else: 522 return string 523 524def make_wrappable(html, wrap_limit=60, 525 split_on=';?&@!$#-/\\"\''): 526 # Currently using <wbr>, maybe should use ​ 527 # http://www.cs.tut.fi/~jkorpela/html/nobr.html 528 if len(html) <= wrap_limit: 529 return html 530 words = html.split() 531 new_words = [] 532 for word in words: 533 wrapped_word = '' 534 while len(word) > wrap_limit: 535 for char in split_on: 536 if char in word: 537 first, rest = word.split(char, 1) 538 wrapped_word += first+char+'<wbr>' 539 word = rest 540 break 541 else: 542 for i in range(0, len(word), wrap_limit): 543 wrapped_word += word[i:i+wrap_limit]+'<wbr>' 544 word = '' 545 wrapped_word += word 546 new_words.append(wrapped_word) 547 return ' '.join(new_words) 548 549def make_pre_wrappable(html, wrap_limit=60, 550 split_on=';?&@!$#-/\\"\''): 551 """ 552 Like ``make_wrappable()`` but intended for text that will 553 go in a ``<pre>`` block, so wrap on a line-by-line basis. 554 """ 555 lines = html.splitlines() 556 new_lines = [] 557 for line in lines: 558 if len(line) > wrap_limit: 559 for char in split_on: 560 if char in line: 561 parts = line.split(char) 562 line = '<wbr>'.join(parts) 563 break 564 new_lines.append(line) 565 return '\n'.join(lines) 566