1"""More comprehensive traceback formatting for Python scripts.
2
3To enable this module, do:
4
5    import cgitb; cgitb.enable()
6
7at the top of your script.  The optional arguments to enable() are:
8
9    display     - if true, tracebacks are displayed in the web browser
10    logdir      - if set, tracebacks are written to files in this directory
11    context     - number of lines of source code to show for each stack frame
12    format      - 'text' or 'html' controls the output format
13
14By default, tracebacks are displayed but not saved, the context is 5 lines
15and the output format is 'html' (for backwards compatibility with the
16original use of this module)
17
18Alternatively, if you have caught an exception and want cgitb to display it
19for you, call cgitb.handler().  The optional argument to handler() is a
203-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
21The default handler displays output as HTML.
22
23"""
24import inspect
25import keyword
26import linecache
27import os
28import pydoc
29import sys
30import tempfile
31import time
32import tokenize
33import traceback
34
35def reset():
36    """Return a string that resets the CGI and browser to a known state."""
37    return '''<!--: spam
38Content-Type: text/html
39
40<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> -->
41<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --> -->
42</font> </font> </font> </script> </object> </blockquote> </pre>
43</table> </table> </table> </table> </table> </font> </font> </font>'''
44
45__UNDEF__ = []                          # a special sentinel object
46def small(text):
47    if text:
48        return '<small>' + text + '</small>'
49    else:
50        return ''
51
52def strong(text):
53    if text:
54        return '<strong>' + text + '</strong>'
55    else:
56        return ''
57
58def grey(text):
59    if text:
60        return '<font color="#909090">' + text + '</font>'
61    else:
62        return ''
63
64def lookup(name, frame, locals):
65    """Find the value for a given name in the given environment."""
66    if name in locals:
67        return 'local', locals[name]
68    if name in frame.f_globals:
69        return 'global', frame.f_globals[name]
70    if '__builtins__' in frame.f_globals:
71        builtins = frame.f_globals['__builtins__']
72        if type(builtins) is type({}):
73            if name in builtins:
74                return 'builtin', builtins[name]
75        else:
76            if hasattr(builtins, name):
77                return 'builtin', getattr(builtins, name)
78    return None, __UNDEF__
79
80def scanvars(reader, frame, locals):
81    """Scan one logical line of Python and look up values of variables used."""
82    vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
83    for ttype, token, start, end, line in tokenize.generate_tokens(reader):
84        if ttype == tokenize.NEWLINE: break
85        if ttype == tokenize.NAME and token not in keyword.kwlist:
86            if lasttoken == '.':
87                if parent is not __UNDEF__:
88                    value = getattr(parent, token, __UNDEF__)
89                    vars.append((prefix + token, prefix, value))
90            else:
91                where, value = lookup(token, frame, locals)
92                vars.append((token, where, value))
93        elif token == '.':
94            prefix += lasttoken + '.'
95            parent = value
96        else:
97            parent, prefix = None, ''
98        lasttoken = token
99    return vars
100
101def html(einfo, context=5):
102    """Return a nice HTML document describing a given traceback."""
103    etype, evalue, etb = einfo
104    if isinstance(etype, type):
105        etype = etype.__name__
106    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
107    date = time.ctime(time.time())
108    head = '<body bgcolor="#f0f0f8">' + pydoc.html.heading(
109        '<big><big>%s</big></big>' %
110        strong(pydoc.html.escape(str(etype))),
111        '#ffffff', '#6622aa', pyver + '<br>' + date) + '''
112<p>A problem occurred in a Python script.  Here is the sequence of
113function calls leading up to the error, in the order they occurred.</p>'''
114
115    indent = '<tt>' + small('&nbsp;' * 5) + '&nbsp;</tt>'
116    frames = []
117    records = inspect.getinnerframes(etb, context)
118    for frame, file, lnum, func, lines, index in records:
119        if file:
120            file = os.path.abspath(file)
121            link = '<a href="file://%s">%s</a>' % (file, pydoc.html.escape(file))
122        else:
123            file = link = '?'
124        args, varargs, varkw, locals = inspect.getargvalues(frame)
125        call = ''
126        if func != '?':
127            call = 'in ' + strong(func) + \
128                inspect.formatargvalues(args, varargs, varkw, locals,
129                    formatvalue=lambda value: '=' + pydoc.html.repr(value))
130
131        highlight = {}
132        def reader(lnum=[lnum]):
133            highlight[lnum[0]] = 1
134            try: return linecache.getline(file, lnum[0])
135            finally: lnum[0] += 1
136        vars = scanvars(reader, frame, locals)
137
138        rows = ['<tr><td bgcolor="#d8bbff">%s%s %s</td></tr>' %
139                ('<big>&nbsp;</big>', link, call)]
140        if index is not None:
141            i = lnum - index
142            for line in lines:
143                num = small('&nbsp;' * (5-len(str(i))) + str(i)) + '&nbsp;'
144                if i in highlight:
145                    line = '<tt>=&gt;%s%s</tt>' % (num, pydoc.html.preformat(line))
146                    rows.append('<tr><td bgcolor="#ffccee">%s</td></tr>' % line)
147                else:
148                    line = '<tt>&nbsp;&nbsp;%s%s</tt>' % (num, pydoc.html.preformat(line))
149                    rows.append('<tr><td>%s</td></tr>' % grey(line))
150                i += 1
151
152        done, dump = {}, []
153        for name, where, value in vars:
154            if name in done: continue
155            done[name] = 1
156            if value is not __UNDEF__:
157                if where in ('global', 'builtin'):
158                    name = ('<em>%s</em> ' % where) + strong(name)
159                elif where == 'local':
160                    name = strong(name)
161                else:
162                    name = where + strong(name.split('.')[-1])
163                dump.append('%s&nbsp;= %s' % (name, pydoc.html.repr(value)))
164            else:
165                dump.append(name + ' <em>undefined</em>')
166
167        rows.append('<tr><td>%s</td></tr>' % small(grey(', '.join(dump))))
168        frames.append('''
169<table width="100%%" cellspacing=0 cellpadding=0 border=0>
170%s</table>''' % '\n'.join(rows))
171
172    exception = ['<p>%s: %s' % (strong(pydoc.html.escape(str(etype))),
173                                pydoc.html.escape(str(evalue)))]
174    for name in dir(evalue):
175        if name[:1] == '_': continue
176        value = pydoc.html.repr(getattr(evalue, name))
177        exception.append('\n<br>%s%s&nbsp;=\n%s' % (indent, name, value))
178
179    return head + ''.join(frames) + ''.join(exception) + '''
180
181
182<!-- The above is a description of an error in a Python program, formatted
183     for a Web browser because the 'cgitb' module was enabled.  In case you
184     are not reading this in a Web browser, here is the original traceback:
185
186%s
187-->
188''' % pydoc.html.escape(
189          ''.join(traceback.format_exception(etype, evalue, etb)))
190
191def text(einfo, context=5):
192    """Return a plain text document describing a given traceback."""
193    etype, evalue, etb = einfo
194    if isinstance(etype, type):
195        etype = etype.__name__
196    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
197    date = time.ctime(time.time())
198    head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + '''
199A problem occurred in a Python script.  Here is the sequence of
200function calls leading up to the error, in the order they occurred.
201'''
202
203    frames = []
204    records = inspect.getinnerframes(etb, context)
205    for frame, file, lnum, func, lines, index in records:
206        file = file and os.path.abspath(file) or '?'
207        args, varargs, varkw, locals = inspect.getargvalues(frame)
208        call = ''
209        if func != '?':
210            call = 'in ' + func + \
211                inspect.formatargvalues(args, varargs, varkw, locals,
212                    formatvalue=lambda value: '=' + pydoc.text.repr(value))
213
214        highlight = {}
215        def reader(lnum=[lnum]):
216            highlight[lnum[0]] = 1
217            try: return linecache.getline(file, lnum[0])
218            finally: lnum[0] += 1
219        vars = scanvars(reader, frame, locals)
220
221        rows = [' %s %s' % (file, call)]
222        if index is not None:
223            i = lnum - index
224            for line in lines:
225                num = '%5d ' % i
226                rows.append(num+line.rstrip())
227                i += 1
228
229        done, dump = {}, []
230        for name, where, value in vars:
231            if name in done: continue
232            done[name] = 1
233            if value is not __UNDEF__:
234                if where == 'global': name = 'global ' + name
235                elif where != 'local': name = where + name.split('.')[-1]
236                dump.append('%s = %s' % (name, pydoc.text.repr(value)))
237            else:
238                dump.append(name + ' undefined')
239
240        rows.append('\n'.join(dump))
241        frames.append('\n%s\n' % '\n'.join(rows))
242
243    exception = ['%s: %s' % (str(etype), str(evalue))]
244    for name in dir(evalue):
245        value = pydoc.text.repr(getattr(evalue, name))
246        exception.append('\n%s%s = %s' % (" "*4, name, value))
247
248    return head + ''.join(frames) + ''.join(exception) + '''
249
250The above is a description of an error in a Python program.  Here is
251the original traceback:
252
253%s
254''' % ''.join(traceback.format_exception(etype, evalue, etb))
255
256class Hook:
257    """A hook to replace sys.excepthook that shows tracebacks in HTML."""
258
259    def __init__(self, display=1, logdir=None, context=5, file=None,
260                 format="html"):
261        self.display = display          # send tracebacks to browser if true
262        self.logdir = logdir            # log tracebacks to files if not None
263        self.context = context          # number of source code lines per frame
264        self.file = file or sys.stdout  # place to send the output
265        self.format = format
266
267    def __call__(self, etype, evalue, etb):
268        self.handle((etype, evalue, etb))
269
270    def handle(self, info=None):
271        info = info or sys.exc_info()
272        if self.format == "html":
273            self.file.write(reset())
274
275        formatter = (self.format=="html") and html or text
276        plain = False
277        try:
278            doc = formatter(info, self.context)
279        except:                         # just in case something goes wrong
280            doc = ''.join(traceback.format_exception(*info))
281            plain = True
282
283        if self.display:
284            if plain:
285                doc = doc.replace('&', '&amp;').replace('<', '&lt;')
286                self.file.write('<pre>' + doc + '</pre>\n')
287            else:
288                self.file.write(doc + '\n')
289        else:
290            self.file.write('<p>A problem occurred in a Python script.\n')
291
292        if self.logdir is not None:
293            suffix = ['.txt', '.html'][self.format=="html"]
294            (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
295
296            try:
297                with os.fdopen(fd, 'w') as file:
298                    file.write(doc)
299                msg = '%s contains the description of this error.' % path
300            except:
301                msg = 'Tried to save traceback to %s, but failed.' % path
302
303            if self.format == 'html':
304                self.file.write('<p>%s</p>\n' % msg)
305            else:
306                self.file.write(msg + '\n')
307        try:
308            self.file.flush()
309        except: pass
310
311handler = Hook().handle
312def enable(display=1, logdir=None, context=5, format="html"):
313    """Install an exception handler that formats tracebacks as HTML.
314
315    The optional argument 'display' can be set to 0 to suppress sending the
316    traceback to the browser, and 'logdir' can be set to a directory to cause
317    tracebacks to be written to files there."""
318    sys.excepthook = Hook(display=display, logdir=logdir,
319                          context=context, format=format)
320