| # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
| # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
| |
| """ |
| Formatters for the exception data that comes from ExceptionCollector. |
| """ |
| # @@: TODO: |
| # Use this: http://www.zope.org/Members/tino/VisualTraceback/VisualTracebackNews |
| |
| import cgi |
| import six |
| import re |
| from paste.util import PySourceColor |
| |
| def html_quote(s): |
| return cgi.escape(str(s), True) |
| |
| class AbstractFormatter(object): |
| |
| general_data_order = ['object', 'source_url'] |
| |
| def __init__(self, show_hidden_frames=False, |
| include_reusable=True, |
| show_extra_data=True, |
| trim_source_paths=()): |
| self.show_hidden_frames = show_hidden_frames |
| self.trim_source_paths = trim_source_paths |
| self.include_reusable = include_reusable |
| self.show_extra_data = show_extra_data |
| |
| def format_collected_data(self, exc_data): |
| general_data = {} |
| if self.show_extra_data: |
| for name, value_list in exc_data.extra_data.items(): |
| if isinstance(name, tuple): |
| importance, title = name |
| else: |
| importance, title = 'normal', name |
| for value in value_list: |
| general_data[(importance, name)] = self.format_extra_data( |
| importance, title, value) |
| lines = [] |
| frames = self.filter_frames(exc_data.frames) |
| for frame in frames: |
| sup = frame.supplement |
| if sup: |
| if sup.object: |
| general_data[('important', 'object')] = self.format_sup_object( |
| sup.object) |
| if sup.source_url: |
| general_data[('important', 'source_url')] = self.format_sup_url( |
| sup.source_url) |
| if sup.line: |
| lines.append(self.format_sup_line_pos(sup.line, sup.column)) |
| if sup.expression: |
| lines.append(self.format_sup_expression(sup.expression)) |
| if sup.warnings: |
| for warning in sup.warnings: |
| lines.append(self.format_sup_warning(warning)) |
| if sup.info: |
| lines.extend(self.format_sup_info(sup.info)) |
| if frame.supplement_exception: |
| lines.append('Exception in supplement:') |
| lines.append(self.quote_long(frame.supplement_exception)) |
| if frame.traceback_info: |
| lines.append(self.format_traceback_info(frame.traceback_info)) |
| filename = frame.filename |
| if filename and self.trim_source_paths: |
| for path, repl in self.trim_source_paths: |
| if filename.startswith(path): |
| filename = repl + filename[len(path):] |
| break |
| lines.append(self.format_source_line(filename or '?', frame)) |
| source = frame.get_source_line() |
| long_source = frame.get_source_line(2) |
| if source: |
| lines.append(self.format_long_source( |
| source, long_source)) |
| etype = exc_data.exception_type |
| if not isinstance(etype, six.string_types): |
| etype = etype.__name__ |
| exc_info = self.format_exception_info( |
| etype, |
| exc_data.exception_value) |
| data_by_importance = {'important': [], 'normal': [], |
| 'supplemental': [], 'extra': []} |
| for (importance, name), value in general_data.items(): |
| data_by_importance[importance].append( |
| (name, value)) |
| for value in data_by_importance.values(): |
| value.sort() |
| return self.format_combine(data_by_importance, lines, exc_info) |
| |
| def filter_frames(self, frames): |
| """ |
| Removes any frames that should be hidden, according to the |
| values of traceback_hide, self.show_hidden_frames, and the |
| hidden status of the final frame. |
| """ |
| if self.show_hidden_frames: |
| return frames |
| new_frames = [] |
| hidden = False |
| for frame in frames: |
| hide = frame.traceback_hide |
| # @@: It would be nice to signal a warning if an unknown |
| # hide string was used, but I'm not sure where to put |
| # that warning. |
| if hide == 'before': |
| new_frames = [] |
| hidden = False |
| elif hide == 'before_and_this': |
| new_frames = [] |
| hidden = False |
| continue |
| elif hide == 'reset': |
| hidden = False |
| elif hide == 'reset_and_this': |
| hidden = False |
| continue |
| elif hide == 'after': |
| hidden = True |
| elif hide == 'after_and_this': |
| hidden = True |
| continue |
| elif hide: |
| continue |
| elif hidden: |
| continue |
| new_frames.append(frame) |
| if frames[-1] not in new_frames: |
| # We must include the last frame; that we don't indicates |
| # that the error happened where something was "hidden", |
| # so we just have to show everything |
| return frames |
| return new_frames |
| |
| def pretty_string_repr(self, s): |
| """ |
| Formats the string as a triple-quoted string when it contains |
| newlines. |
| """ |
| if '\n' in s: |
| s = repr(s) |
| s = s[0]*3 + s[1:-1] + s[-1]*3 |
| s = s.replace('\\n', '\n') |
| return s |
| else: |
| return repr(s) |
| |
| def long_item_list(self, lst): |
| """ |
| Returns true if the list contains items that are long, and should |
| be more nicely formatted. |
| """ |
| how_many = 0 |
| for item in lst: |
| if len(repr(item)) > 40: |
| how_many += 1 |
| if how_many >= 3: |
| return True |
| return False |
| |
| class TextFormatter(AbstractFormatter): |
| |
| def quote(self, s): |
| return s |
| def quote_long(self, s): |
| return s |
| def emphasize(self, s): |
| return s |
| def format_sup_object(self, obj): |
| return 'In object: %s' % self.emphasize(self.quote(repr(obj))) |
| def format_sup_url(self, url): |
| return 'URL: %s' % self.quote(url) |
| def format_sup_line_pos(self, line, column): |
| if column: |
| return self.emphasize('Line %i, Column %i' % (line, column)) |
| else: |
| return self.emphasize('Line %i' % line) |
| def format_sup_expression(self, expr): |
| return self.emphasize('In expression: %s' % self.quote(expr)) |
| def format_sup_warning(self, warning): |
| return 'Warning: %s' % self.quote(warning) |
| def format_sup_info(self, info): |
| return [self.quote_long(info)] |
| def format_source_line(self, filename, frame): |
| return 'File %r, line %s in %s' % ( |
| filename, frame.lineno or '?', frame.name or '?') |
| def format_long_source(self, source, long_source): |
| return self.format_source(source) |
| def format_source(self, source_line): |
| return ' ' + self.quote(source_line.strip()) |
| def format_exception_info(self, etype, evalue): |
| return self.emphasize( |
| '%s: %s' % (self.quote(etype), self.quote(evalue))) |
| def format_traceback_info(self, info): |
| return info |
| |
| def format_combine(self, data_by_importance, lines, exc_info): |
| lines[:0] = [value for n, value in data_by_importance['important']] |
| lines.append(exc_info) |
| for name in 'normal', 'supplemental', 'extra': |
| lines.extend([value for n, value in data_by_importance[name]]) |
| return self.format_combine_lines(lines) |
| |
| def format_combine_lines(self, lines): |
| return '\n'.join(lines) |
| |
| def format_extra_data(self, importance, title, value): |
| if isinstance(value, str): |
| s = self.pretty_string_repr(value) |
| if '\n' in s: |
| return '%s:\n%s' % (title, s) |
| else: |
| return '%s: %s' % (title, s) |
| elif isinstance(value, dict): |
| lines = ['\n', title, '-'*len(title)] |
| items = value.items() |
| items.sort() |
| for n, v in items: |
| try: |
| v = repr(v) |
| except Exception as e: |
| v = 'Cannot display: %s' % e |
| v = truncate(v) |
| lines.append(' %s: %s' % (n, v)) |
| return '\n'.join(lines) |
| elif (isinstance(value, (list, tuple)) |
| and self.long_item_list(value)): |
| parts = [truncate(repr(v)) for v in value] |
| return '%s: [\n %s]' % ( |
| title, ',\n '.join(parts)) |
| else: |
| return '%s: %s' % (title, truncate(repr(value))) |
| |
| class HTMLFormatter(TextFormatter): |
| |
| def quote(self, s): |
| return html_quote(s) |
| def quote_long(self, s): |
| return '<pre>%s</pre>' % self.quote(s) |
| def emphasize(self, s): |
| return '<b>%s</b>' % s |
| def format_sup_url(self, url): |
| return 'URL: <a href="%s">%s</a>' % (url, url) |
| def format_combine_lines(self, lines): |
| return '<br>\n'.join(lines) |
| def format_source_line(self, filename, frame): |
| name = self.quote(frame.name or '?') |
| return 'Module <span class="module" title="%s">%s</span>:<b>%s</b> in <code>%s</code>' % ( |
| filename, frame.modname or '?', frame.lineno or '?', |
| name) |
| return 'File %r, line %s in <tt>%s</tt>' % ( |
| filename, frame.lineno, name) |
| def format_long_source(self, source, long_source): |
| q_long_source = str2html(long_source, False, 4, True) |
| q_source = str2html(source, True, 0, False) |
| return ('<code style="display: none" class="source" source-type="long"><a class="switch_source" onclick="return switch_source(this, \'long\')" href="#"><< </a>%s</code>' |
| '<code class="source" source-type="short"><a onclick="return switch_source(this, \'short\')" class="switch_source" href="#">>> </a>%s</code>' |
| % (q_long_source, |
| q_source)) |
| def format_source(self, source_line): |
| return ' <code class="source">%s</code>' % self.quote(source_line.strip()) |
| def format_traceback_info(self, info): |
| return '<pre>%s</pre>' % self.quote(info) |
| |
| def format_extra_data(self, importance, title, value): |
| if isinstance(value, str): |
| s = self.pretty_string_repr(value) |
| if '\n' in s: |
| return '%s:<br><pre>%s</pre>' % (title, self.quote(s)) |
| else: |
| return '%s: <tt>%s</tt>' % (title, self.quote(s)) |
| elif isinstance(value, dict): |
| return self.zebra_table(title, value) |
| elif (isinstance(value, (list, tuple)) |
| and self.long_item_list(value)): |
| return '%s: <tt>[<br>\n %s]</tt>' % ( |
| title, ',<br> '.join(map(self.quote, map(repr, value)))) |
| else: |
| return '%s: <tt>%s</tt>' % (title, self.quote(repr(value))) |
| |
| def format_combine(self, data_by_importance, lines, exc_info): |
| lines[:0] = [value for n, value in data_by_importance['important']] |
| lines.append(exc_info) |
| for name in 'normal', 'supplemental': |
| lines.extend([value for n, value in data_by_importance[name]]) |
| if data_by_importance['extra']: |
| lines.append( |
| '<script type="text/javascript">\nshow_button(\'extra_data\', \'extra data\');\n</script>\n' + |
| '<div id="extra_data" class="hidden-data">\n') |
| lines.extend([value for n, value in data_by_importance['extra']]) |
| lines.append('</div>') |
| text = self.format_combine_lines(lines) |
| if self.include_reusable: |
| return error_css + hide_display_js + text |
| else: |
| # Usually because another error is already on this page, |
| # and so the js & CSS are unneeded |
| return text |
| |
| def zebra_table(self, title, rows, table_class="variables"): |
| if isinstance(rows, dict): |
| rows = rows.items() |
| rows.sort() |
| table = ['<table class="%s">' % table_class, |
| '<tr class="header"><th colspan="2">%s</th></tr>' |
| % self.quote(title)] |
| odd = False |
| for name, value in rows: |
| try: |
| value = repr(value) |
| except Exception as e: |
| value = 'Cannot print: %s' % e |
| odd = not odd |
| table.append( |
| '<tr class="%s"><td>%s</td>' |
| % (odd and 'odd' or 'even', self.quote(name))) |
| table.append( |
| '<td><tt>%s</tt></td></tr>' |
| % make_wrappable(self.quote(truncate(value)))) |
| table.append('</table>') |
| return '\n'.join(table) |
| |
| hide_display_js = r''' |
| <script type="text/javascript"> |
| function hide_display(id) { |
| var el = document.getElementById(id); |
| if (el.className == "hidden-data") { |
| el.className = ""; |
| return true; |
| } else { |
| el.className = "hidden-data"; |
| return false; |
| } |
| } |
| document.write('<style type="text/css">\n'); |
| document.write('.hidden-data {display: none}\n'); |
| document.write('</style>\n'); |
| function show_button(toggle_id, name) { |
| document.write('<a href="#' + toggle_id |
| + '" onclick="javascript:hide_display(\'' + toggle_id |
| + '\')" class="button">' + name + '</a><br>'); |
| } |
| |
| function switch_source(el, hide_type) { |
| while (el) { |
| if (el.getAttribute && |
| el.getAttribute('source-type') == hide_type) { |
| break; |
| } |
| el = el.parentNode; |
| } |
| if (! el) { |
| return false; |
| } |
| el.style.display = 'none'; |
| if (hide_type == 'long') { |
| while (el) { |
| if (el.getAttribute && |
| el.getAttribute('source-type') == 'short') { |
| break; |
| } |
| el = el.nextSibling; |
| } |
| } else { |
| while (el) { |
| if (el.getAttribute && |
| el.getAttribute('source-type') == 'long') { |
| break; |
| } |
| el = el.previousSibling; |
| } |
| } |
| if (el) { |
| el.style.display = ''; |
| } |
| return false; |
| } |
| |
| </script>''' |
| |
| |
| error_css = """ |
| <style type="text/css"> |
| body { |
| font-family: Helvetica, sans-serif; |
| } |
| |
| table { |
| width: 100%; |
| } |
| |
| tr.header { |
| background-color: #006; |
| color: #fff; |
| } |
| |
| tr.even { |
| background-color: #ddd; |
| } |
| |
| table.variables td { |
| vertical-align: top; |
| overflow: auto; |
| } |
| |
| a.button { |
| background-color: #ccc; |
| border: 2px outset #aaa; |
| color: #000; |
| text-decoration: none; |
| } |
| |
| a.button:hover { |
| background-color: #ddd; |
| } |
| |
| code.source { |
| color: #006; |
| } |
| |
| a.switch_source { |
| color: #090; |
| text-decoration: none; |
| } |
| |
| a.switch_source:hover { |
| background-color: #ddd; |
| } |
| |
| .source-highlight { |
| background-color: #ff9; |
| } |
| |
| </style> |
| """ |
| |
| def format_html(exc_data, include_hidden_frames=False, **ops): |
| if not include_hidden_frames: |
| return HTMLFormatter(**ops).format_collected_data(exc_data) |
| short_er = format_html(exc_data, show_hidden_frames=False, **ops) |
| # @@: This should have a way of seeing if the previous traceback |
| # was actually trimmed at all |
| ops['include_reusable'] = False |
| ops['show_extra_data'] = False |
| long_er = format_html(exc_data, show_hidden_frames=True, **ops) |
| text_er = format_text(exc_data, show_hidden_frames=True, **ops) |
| return """ |
| %s |
| <br> |
| <script type="text/javascript"> |
| show_button('full_traceback', 'full traceback') |
| </script> |
| <div id="full_traceback" class="hidden-data"> |
| %s |
| </div> |
| <br> |
| <script type="text/javascript"> |
| show_button('text_version', 'text version') |
| </script> |
| <div id="text_version" class="hidden-data"> |
| <textarea style="width: 100%%" rows=10 cols=60>%s</textarea> |
| </div> |
| """ % (short_er, long_er, cgi.escape(text_er)) |
| |
| def format_text(exc_data, **ops): |
| return TextFormatter(**ops).format_collected_data(exc_data) |
| |
| whitespace_re = re.compile(r' +') |
| pre_re = re.compile(r'</?pre.*?>') |
| error_re = re.compile(r'<h3>ERROR: .*?</h3>') |
| |
| def str2html(src, strip=False, indent_subsequent=0, |
| highlight_inner=False): |
| """ |
| Convert a string to HTML. Try to be really safe about it, |
| returning a quoted version of the string if nothing else works. |
| """ |
| try: |
| return _str2html(src, strip=strip, |
| indent_subsequent=indent_subsequent, |
| highlight_inner=highlight_inner) |
| except: |
| return html_quote(src) |
| |
| def _str2html(src, strip=False, indent_subsequent=0, |
| highlight_inner=False): |
| if strip: |
| src = src.strip() |
| orig_src = src |
| try: |
| src = PySourceColor.str2html(src, form='snip') |
| src = error_re.sub('', src) |
| src = pre_re.sub('', src) |
| src = re.sub(r'^[\n\r]{0,1}', '', src) |
| src = re.sub(r'[\n\r]{0,1}$', '', src) |
| except: |
| src = html_quote(orig_src) |
| lines = src.splitlines() |
| if len(lines) == 1: |
| return lines[0] |
| indent = ' '*indent_subsequent |
| for i in range(1, len(lines)): |
| lines[i] = indent+lines[i] |
| if highlight_inner and i == len(lines)/2: |
| lines[i] = '<span class="source-highlight">%s</span>' % lines[i] |
| src = '<br>\n'.join(lines) |
| src = whitespace_re.sub( |
| lambda m: ' '*(len(m.group(0))-1) + ' ', src) |
| return src |
| |
| def truncate(string, limit=1000): |
| """ |
| Truncate the string to the limit number of |
| characters |
| """ |
| if len(string) > limit: |
| return string[:limit-20]+'...'+string[-17:] |
| else: |
| return string |
| |
| def make_wrappable(html, wrap_limit=60, |
| split_on=';?&@!$#-/\\"\''): |
| # Currently using <wbr>, maybe should use ​ |
| # http://www.cs.tut.fi/~jkorpela/html/nobr.html |
| if len(html) <= wrap_limit: |
| return html |
| words = html.split() |
| new_words = [] |
| for word in words: |
| wrapped_word = '' |
| while len(word) > wrap_limit: |
| for char in split_on: |
| if char in word: |
| first, rest = word.split(char, 1) |
| wrapped_word += first+char+'<wbr>' |
| word = rest |
| break |
| else: |
| for i in range(0, len(word), wrap_limit): |
| wrapped_word += word[i:i+wrap_limit]+'<wbr>' |
| word = '' |
| wrapped_word += word |
| new_words.append(wrapped_word) |
| return ' '.join(new_words) |
| |
| def make_pre_wrappable(html, wrap_limit=60, |
| split_on=';?&@!$#-/\\"\''): |
| """ |
| Like ``make_wrappable()`` but intended for text that will |
| go in a ``<pre>`` block, so wrap on a line-by-line basis. |
| """ |
| lines = html.splitlines() |
| new_lines = [] |
| for line in lines: |
| if len(line) > wrap_limit: |
| for char in split_on: |
| if char in line: |
| parts = line.split(char) |
| line = '<wbr>'.join(parts) |
| break |
| new_lines.append(line) |
| return '\n'.join(lines) |