cgitb.py 11.7 KB
Newer Older
1
"""More comprehensive traceback formatting for Python scripts.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
2 3 4 5 6

To enable this module, do:

    import cgitb; cgitb.enable()

7
at the top of your script.  The optional arguments to enable() are:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
8 9 10

    display     - if true, tracebacks are displayed in the web browser
    logdir      - if set, tracebacks are written to files in this directory
Ka-Ping Yee's avatar
Ka-Ping Yee committed
11
    context     - number of lines of source code to show for each stack frame
Tim Peters's avatar
Tim Peters committed
12
    format      - 'text' or 'html' controls the output format
Ka-Ping Yee's avatar
Ka-Ping Yee committed
13

14 15 16
By default, tracebacks are displayed but not saved, the context is 5 lines
and the output format is 'html' (for backwards compatibility with the
original use of this module)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
17 18

Alternatively, if you have caught an exception and want cgitb to display it
19 20 21
for you, call cgitb.handler().  The optional argument to handler() is a
3-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
The default handler displays output as HTML.
Ka-Ping Yee's avatar
Ka-Ping Yee committed
22

23 24 25 26 27 28
"""
import inspect
import keyword
import linecache
import os
import pydoc
29
import sys
30 31 32 33 34
import tempfile
import time
import tokenize
import traceback
import types
35

Ka-Ping Yee's avatar
Ka-Ping Yee committed
36 37 38 39 40
def reset():
    """Return a string that resets the CGI and browser to a known state."""
    return '''<!--: spam
Content-Type: text/html

Ka-Ping Yee's avatar
Ka-Ping Yee committed
41 42
<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> -->
<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --> -->
Ka-Ping Yee's avatar
Ka-Ping Yee committed
43 44 45
</font> </font> </font> </script> </object> </blockquote> </pre>
</table> </table> </table> </table> </table> </font> </font> </font>'''

Ka-Ping Yee's avatar
Ka-Ping Yee committed
46
__UNDEF__ = []                          # a special sentinel object
47 48 49 50 51
def small(text):
    if text:
        return '<small>' + text + '</small>'
    else:
        return ''
52

53 54 55 56 57
def strong(text):
    if text:
        return '<strong>' + text + '</strong>'
    else:
        return ''
58

59 60 61 62 63
def grey(text):
    if text:
        return '<font color="#909090">' + text + '</font>'
    else:
        return ''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
64 65 66 67 68 69 70

def lookup(name, frame, locals):
    """Find the value for a given name in the given environment."""
    if name in locals:
        return 'local', locals[name]
    if name in frame.f_globals:
        return 'global', frame.f_globals[name]
71 72 73 74 75 76 77 78
    if '__builtins__' in frame.f_globals:
        builtins = frame.f_globals['__builtins__']
        if type(builtins) is type({}):
            if name in builtins:
                return 'builtin', builtins[name]
        else:
            if hasattr(builtins, name):
                return 'builtin', getattr(builtins, name)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
79 80 81 82
    return None, __UNDEF__

def scanvars(reader, frame, locals):
    """Scan one logical line of Python and look up values of variables used."""
83
    vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
Ka-Ping Yee's avatar
Ka-Ping Yee committed
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
    for ttype, token, start, end, line in tokenize.generate_tokens(reader):
        if ttype == tokenize.NEWLINE: break
        if ttype == tokenize.NAME and token not in keyword.kwlist:
            if lasttoken == '.':
                if parent is not __UNDEF__:
                    value = getattr(parent, token, __UNDEF__)
                    vars.append((prefix + token, prefix, value))
            else:
                where, value = lookup(token, frame, locals)
                vars.append((token, where, value))
        elif token == '.':
            prefix += lasttoken + '.'
            parent = value
        else:
            parent, prefix = None, ''
        lasttoken = token
    return vars

102
def html(einfo, context=5):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
103
    """Return a nice HTML document describing a given traceback."""
104
    etype, evalue, etb = einfo
105
    if isinstance(etype, type):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
106 107 108
        etype = etype.__name__
    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
    date = time.ctime(time.time())
Ka-Ping Yee's avatar
Ka-Ping Yee committed
109
    head = '<body bgcolor="#f0f0f8">' + pydoc.html.heading(
110 111
        '<big><big>%s</big></big>' %
        strong(pydoc.html.escape(str(etype))),
Ka-Ping Yee's avatar
Ka-Ping Yee committed
112 113
        '#ffffff', '#6622aa', pyver + '<br>' + date) + '''
<p>A problem occurred in a Python script.  Here is the sequence of
114
function calls leading up to the error, in the order they occurred.</p>'''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
115

Ka-Ping Yee's avatar
Ka-Ping Yee committed
116
    indent = '<tt>' + small('&nbsp;' * 5) + '&nbsp;</tt>'
Ka-Ping Yee's avatar
Ka-Ping Yee committed
117 118 119
    frames = []
    records = inspect.getinnerframes(etb, context)
    for frame, file, lnum, func, lines, index in records:
120 121 122 123 124
        if file:
            file = os.path.abspath(file)
            link = '<a href="file://%s">%s</a>' % (file, pydoc.html.escape(file))
        else:
            file = link = '?'
Ka-Ping Yee's avatar
Ka-Ping Yee committed
125
        args, varargs, varkw, locals = inspect.getargvalues(frame)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
        call = ''
        if func != '?':
            call = 'in ' + strong(func) + \
                inspect.formatargvalues(args, varargs, varkw, locals,
                    formatvalue=lambda value: '=' + pydoc.html.repr(value))

        highlight = {}
        def reader(lnum=[lnum]):
            highlight[lnum[0]] = 1
            try: return linecache.getline(file, lnum[0])
            finally: lnum[0] += 1
        vars = scanvars(reader, frame, locals)

        rows = ['<tr><td bgcolor="#d8bbff">%s%s %s</td></tr>' %
                ('<big>&nbsp;</big>', link, call)]
Ka-Ping Yee's avatar
Ka-Ping Yee committed
141 142 143
        if index is not None:
            i = lnum - index
            for line in lines:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
144 145
                num = small('&nbsp;' * (5-len(str(i))) + str(i)) + '&nbsp;'
                if i in highlight:
Benjamin Peterson's avatar
Benjamin Peterson committed
146
                    line = '<tt>=&gt;%s%s</tt>' % (num, pydoc.html.preformat(line))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
147 148
                    rows.append('<tr><td bgcolor="#ffccee">%s</td></tr>' % line)
                else:
Benjamin Peterson's avatar
Benjamin Peterson committed
149
                    line = '<tt>&nbsp;&nbsp;%s%s</tt>' % (num, pydoc.html.preformat(line))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
150 151 152 153 154 155 156 157
                    rows.append('<tr><td>%s</td></tr>' % grey(line))
                i += 1

        done, dump = {}, []
        for name, where, value in vars:
            if name in done: continue
            done[name] = 1
            if value is not __UNDEF__:
158
                if where in ('global', 'builtin'):
159 160 161 162 163
                    name = ('<em>%s</em> ' % where) + strong(name)
                elif where == 'local':
                    name = strong(name)
                else:
                    name = where + strong(name.split('.')[-1])
Ka-Ping Yee's avatar
Ka-Ping Yee committed
164 165 166 167 168
                dump.append('%s&nbsp;= %s' % (name, pydoc.html.repr(value)))
            else:
                dump.append(name + ' <em>undefined</em>')

        rows.append('<tr><td>%s</td></tr>' % small(grey(', '.join(dump))))
169
        frames.append('''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
170 171 172
<table width="100%%" cellspacing=0 cellpadding=0 border=0>
%s</table>''' % '\n'.join(rows))

173 174
    exception = ['<p>%s: %s' % (strong(pydoc.html.escape(str(etype))),
                                pydoc.html.escape(str(evalue)))]
175 176 177 178
    for name in dir(evalue):
        if name[:1] == '_': continue
        value = pydoc.html.repr(getattr(evalue, name))
        exception.append('\n<br>%s%s&nbsp;=\n%s' % (indent, name, value))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
179 180 181 182

    return head + ''.join(frames) + ''.join(exception) + '''


Ka-Ping Yee's avatar
Ka-Ping Yee committed
183 184 185
<!-- The above is a description of an error in a Python program, formatted
     for a Web browser because the 'cgitb' module was enabled.  In case you
     are not reading this in a Web browser, here is the original traceback:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
186 187 188

%s
-->
189 190
''' % pydoc.html.escape(
          ''.join(traceback.format_exception(etype, evalue, etb)))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
191

192
def text(einfo, context=5):
193
    """Return a plain text document describing a given traceback."""
194
    etype, evalue, etb = einfo
195
    if isinstance(etype, type):
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
        etype = etype.__name__
    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
    date = time.ctime(time.time())
    head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + '''
A problem occurred in a Python script.  Here is the sequence of
function calls leading up to the error, in the order they occurred.
'''

    frames = []
    records = inspect.getinnerframes(etb, context)
    for frame, file, lnum, func, lines, index in records:
        file = file and os.path.abspath(file) or '?'
        args, varargs, varkw, locals = inspect.getargvalues(frame)
        call = ''
        if func != '?':
            call = 'in ' + func + \
                inspect.formatargvalues(args, varargs, varkw, locals,
                    formatvalue=lambda value: '=' + pydoc.text.repr(value))

        highlight = {}
        def reader(lnum=[lnum]):
            highlight[lnum[0]] = 1
            try: return linecache.getline(file, lnum[0])
            finally: lnum[0] += 1
        vars = scanvars(reader, frame, locals)

        rows = [' %s %s' % (file, call)]
        if index is not None:
            i = lnum - index
            for line in lines:
                num = '%5d ' % i
                rows.append(num+line.rstrip())
                i += 1

        done, dump = {}, []
        for name, where, value in vars:
            if name in done: continue
            done[name] = 1
            if value is not __UNDEF__:
                if where == 'global': name = 'global ' + name
236
                elif where != 'local': name = where + name.split('.')[-1]
237 238 239 240 241 242 243 244
                dump.append('%s = %s' % (name, pydoc.text.repr(value)))
            else:
                dump.append(name + ' undefined')

        rows.append('\n'.join(dump))
        frames.append('\n%s\n' % '\n'.join(rows))

    exception = ['%s: %s' % (str(etype), str(evalue))]
245 246 247
    for name in dir(evalue):
        value = pydoc.text.repr(getattr(evalue, name))
        exception.append('\n%s%s = %s' % (" "*4, name, value))
248 249 250 251 252 253 254 255 256

    return head + ''.join(frames) + ''.join(exception) + '''

The above is a description of an error in a Python program.  Here is
the original traceback:

%s
''' % ''.join(traceback.format_exception(etype, evalue, etb))

Ka-Ping Yee's avatar
Ka-Ping Yee committed
257
class Hook:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
258 259
    """A hook to replace sys.excepthook that shows tracebacks in HTML."""

260 261
    def __init__(self, display=1, logdir=None, context=5, file=None,
                 format="html"):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
262 263
        self.display = display          # send tracebacks to browser if true
        self.logdir = logdir            # log tracebacks to files if not None
Ka-Ping Yee's avatar
Ka-Ping Yee committed
264
        self.context = context          # number of source code lines per frame
265
        self.file = file or sys.stdout  # place to send the output
266
        self.format = format
Ka-Ping Yee's avatar
Ka-Ping Yee committed
267 268 269 270 271 272

    def __call__(self, etype, evalue, etb):
        self.handle((etype, evalue, etb))

    def handle(self, info=None):
        info = info or sys.exc_info()
273 274
        if self.format == "html":
            self.file.write(reset())
Ka-Ping Yee's avatar
Ka-Ping Yee committed
275

276 277
        formatter = (self.format=="html") and html or text
        plain = False
Ka-Ping Yee's avatar
Ka-Ping Yee committed
278
        try:
279
            doc = formatter(info, self.context)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
280
        except:                         # just in case something goes wrong
281 282
            doc = ''.join(traceback.format_exception(*info))
            plain = True
Ka-Ping Yee's avatar
Ka-Ping Yee committed
283 284

        if self.display:
285
            if plain:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
286
                doc = doc.replace('&', '&amp;').replace('<', '&lt;')
287
                self.file.write('<pre>' + doc + '</pre>\n')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
288
            else:
289
                self.file.write(doc + '\n')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
290
        else:
291
            self.file.write('<p>A problem occurred in a Python script.\n')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
292 293

        if self.logdir is not None:
294
            suffix = ['.txt', '.html'][self.format=="html"]
295
            (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
296
            try:
297
                file = os.fdopen(fd, 'w')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
298 299
                file.write(doc)
                file.close()
300
                msg = '<p> %s contains the description of this error.' % path
Ka-Ping Yee's avatar
Ka-Ping Yee committed
301
            except:
302 303 304 305 306
                msg = '<p> Tried to save traceback to %s, but failed.' % path
            self.file.write(msg + '\n')
        try:
            self.file.flush()
        except: pass
Ka-Ping Yee's avatar
Ka-Ping Yee committed
307 308

handler = Hook().handle
309
def enable(display=1, logdir=None, context=5, format="html"):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
310 311 312 313 314
    """Install an exception handler that formats tracebacks as HTML.

    The optional argument 'display' can be set to 0 to suppress sending the
    traceback to the browser, and 'logdir' can be set to a directory to cause
    tracebacks to be written to files there."""
315 316
    sys.excepthook = Hook(display=display, logdir=logdir,
                          context=context, format=format)