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="#">&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
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: '&nbsp;'*(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 &#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
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