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 22
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
23 24

__author__ = 'Ka-Ping Yee'
Jeremy Hylton's avatar
Jeremy Hylton committed
25

Ka-Ping Yee's avatar
Ka-Ping Yee committed
26 27
__version__ = '$Revision$'

28 29
import sys

Ka-Ping Yee's avatar
Ka-Ping Yee committed
30 31 32 33 34
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
35 36
<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
37 38 39
</font> </font> </font> </script> </object> </blockquote> </pre>
</table> </table> </table> </table> </table> </font> </font> </font>'''

Ka-Ping Yee's avatar
Ka-Ping Yee committed
40
__UNDEF__ = []                          # a special sentinel object
41 42 43 44 45
def small(text):
    if text:
        return '<small>' + text + '</small>'
    else:
        return ''
46

47 48 49 50 51
def strong(text):
    if text:
        return '<strong>' + text + '</strong>'
    else:
        return ''
52

53 54 55 56 57
def grey(text):
    if text:
        return '<font color="#909090">' + text + '</font>'
    else:
        return ''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
58 59 60 61 62 63 64

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]
65 66 67 68 69 70 71 72
    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
73 74 75 76 77
    return None, __UNDEF__

def scanvars(reader, frame, locals):
    """Scan one logical line of Python and look up values of variables used."""
    import tokenize, keyword
78
    vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
Ka-Ping Yee's avatar
Ka-Ping Yee committed
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    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

97
def html(einfo, context=5):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
98
    """Return a nice HTML document describing a given traceback."""
99
    import os, time, traceback, linecache, inspect, pydoc
Ka-Ping Yee's avatar
Ka-Ping Yee committed
100

101
    etype, evalue, etb = einfo
102
    if isinstance(etype, type):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
103 104 105
        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
106
    head = '<body bgcolor="#f0f0f8">' + pydoc.html.heading(
107 108
        '<big><big>%s</big></big>' %
        strong(pydoc.html.escape(str(etype))),
Ka-Ping Yee's avatar
Ka-Ping Yee committed
109 110
        '#ffffff', '#6622aa', pyver + '<br>' + date) + '''
<p>A problem occurred in a Python script.  Here is the sequence of
111
function calls leading up to the error, in the order they occurred.</p>'''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
112

Ka-Ping Yee's avatar
Ka-Ping Yee committed
113
    indent = '<tt>' + small('&nbsp;' * 5) + '&nbsp;</tt>'
Ka-Ping Yee's avatar
Ka-Ping Yee committed
114 115 116
    frames = []
    records = inspect.getinnerframes(etb, context)
    for frame, file, lnum, func, lines, index in records:
117 118 119 120 121
        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
122
        args, varargs, varkw, locals = inspect.getargvalues(frame)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
        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
138 139 140
        if index is not None:
            i = lnum - index
            for line in lines:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
141 142 143 144 145 146 147 148 149 150 151 152 153
                num = small('&nbsp;' * (5-len(str(i))) + str(i)) + '&nbsp;'
                line = '<tt>%s%s</tt>' % (num, pydoc.html.preformat(line))
                if i in highlight:
                    rows.append('<tr><td bgcolor="#ffccee">%s</td></tr>' % line)
                else:
                    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__:
154
                if where in ('global', 'builtin'):
155 156 157 158 159
                    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
160 161 162 163 164
                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))))
165
        frames.append('''
Ka-Ping Yee's avatar
Ka-Ping Yee committed
166 167 168
<table width="100%%" cellspacing=0 cellpadding=0 border=0>
%s</table>''' % '\n'.join(rows))

169 170
    exception = ['<p>%s: %s' % (strong(pydoc.html.escape(str(etype))),
                                pydoc.html.escape(str(evalue)))]
171 172 173 174
    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
175 176 177 178 179

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


Ka-Ping Yee's avatar
Ka-Ping Yee committed
180 181 182
<!-- 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
183 184 185

%s
-->
186 187
''' % pydoc.html.escape(
          ''.join(traceback.format_exception(etype, evalue, etb)))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
188

189
def text(einfo, context=5):
190
    """Return a plain text document describing a given traceback."""
191
    import os, time, traceback, linecache, inspect, pydoc
192

193
    etype, evalue, etb = einfo
194
    if isinstance(etype, type):
195 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
        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
235
                elif where != 'local': name = where + name.split('.')[-1]
236 237 238 239 240 241 242 243
                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))]
244 245 246
    for name in dir(evalue):
        value = pydoc.text.repr(getattr(evalue, name))
        exception.append('\n%s%s = %s' % (" "*4, name, value))
247 248 249 250 251 252 253 254 255 256

    import traceback
    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 281
        except:                         # just in case something goes wrong
            import traceback
282 283
            doc = ''.join(traceback.format_exception(*info))
            plain = True
Ka-Ping Yee's avatar
Ka-Ping Yee committed
284 285

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

        if self.logdir is not None:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
295
            import os, tempfile
296
            suffix = ['.txt', '.html'][self.format=="html"]
297
            (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
298
            try:
299
                file = os.fdopen(fd, 'w')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
300 301
                file.write(doc)
                file.close()
302
                msg = '<p> %s contains the description of this error.' % path
Ka-Ping Yee's avatar
Ka-Ping Yee committed
303
            except:
304 305 306 307 308
                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
309 310

handler = Hook().handle
311
def enable(display=1, logdir=None, context=5, format="html"):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
312 313 314 315 316
    """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."""
317 318
    sys.excepthook = Hook(display=display, logdir=logdir,
                          context=context, format=format)