Home | History | Annotate | Download | only in exceptions
      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 """
      5 Formatters for the exception data that comes from ExceptionCollector.
      6 """
      7 # @@: TODO:
      8 # Use this: http://www.zope.org/Members/tino/VisualTraceback/VisualTracebackNews
      9 
     10 import cgi
     11 import six
     12 import re
     13 from paste.util import PySourceColor
     14 
     15 def html_quote(s):
     16     return cgi.escape(str(s), True)
     17 
     18 class 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 
    164 class 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 
    237 class 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="#">&lt;&lt;&nbsp; </a>%s</code>'
    260                 '<code class="source" source-type="short"><a onclick="return switch_source(this, \'short\')" class="switch_source" href="#">&gt;&gt;&nbsp; </a>%s</code>'
    261                 % (q_long_source,
    262                    q_source))
    263     def format_source(self, source_line):
    264         return '&nbsp;&nbsp;<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&nbsp; &nbsp; %s]</tt>' % (
    280                 title, ',<br>&nbsp; &nbsp; '.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 
    326 hide_display_js = r'''
    327 <script type="text/javascript">
    328 function 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 }
    338 document.write('<style type="text/css">\n');
    339 document.write('.hidden-data {display: none}\n');
    340 document.write('</style>\n');
    341 function 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 
    347 function 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 
    385 error_css = """
    386 <style type="text/css">
    387 body {
    388   font-family: Helvetica, sans-serif;
    389 }
    390 
    391 table {
    392   width: 100%;
    393 }
    394 
    395 tr.header {
    396   background-color: #006;
    397   color: #fff;
    398 }
    399 
    400 tr.even {
    401   background-color: #ddd;
    402 }
    403 
    404 table.variables td {
    405   vertical-align: top;
    406   overflow: auto;
    407 }
    408 
    409 a.button {
    410   background-color: #ccc;
    411   border: 2px outset #aaa;
    412   color: #000;
    413   text-decoration: none;
    414 }
    415 
    416 a.button:hover {
    417   background-color: #ddd;
    418 }
    419 
    420 code.source {
    421   color: #006;
    422 }
    423 
    424 a.switch_source {
    425   color: #090;
    426   text-decoration: none;
    427 }
    428 
    429 a.switch_source:hover {
    430   background-color: #ddd;
    431 }
    432 
    433 .source-highlight {
    434   background-color: #ff9;
    435 }
    436 
    437 </style>
    438 """
    439 
    440 def 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 
    468 def format_text(exc_data, **ops):
    469     return TextFormatter(**ops).format_collected_data(exc_data)
    470 
    471 whitespace_re = re.compile(r'  +')
    472 pre_re = re.compile(r'</?pre.*?>')
    473 error_re = re.compile(r'<h3>ERROR: .*?</h3>')
    474 
    475 def 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 
    488 def _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: '&nbsp;'*(len(m.group(0))-1) + ' ', src)
    512     return src
    513 
    514 def 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 
    524 def make_wrappable(html, wrap_limit=60,
    525                    split_on=';?&@!$#-/\\"\''):
    526     # Currently using <wbr>, maybe should use &#8203;
    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 
    549 def 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