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