cgitb.py 11.8 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 97 98
    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

def html((etype, evalue, etb), context=5):
    """Return a nice HTML document describing a given traceback."""
99
    import os, types, time, traceback, linecache, inspect, pydoc
Ka-Ping Yee's avatar
Ka-Ping Yee committed
100 101 102 103 104

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

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

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

    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
-->
Ka-Ping Yee's avatar
Ka-Ping Yee committed
186
''' % ''.join(traceback.format_exception(etype, evalue, etb))
Ka-Ping Yee's avatar
Ka-Ping Yee committed
187

188 189 190 191 192 193 194 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
def text((etype, evalue, etb), context=5):
    """Return a plain text document describing a given traceback."""
    import os, types, time, traceback, linecache, inspect, pydoc

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

    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
256
class Hook:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
257 258
    """A hook to replace sys.excepthook that shows tracebacks in HTML."""

259 260
    def __init__(self, display=1, logdir=None, context=5, file=None,
                 format="html"):
Ka-Ping Yee's avatar
Ka-Ping Yee committed
261 262
        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
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
Ka-Ping Yee's avatar
Ka-Ping Yee committed
266 267 268 269 270 271

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

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

275 276
        formatter = (self.format=="html") and html or text
        plain = False
Ka-Ping Yee's avatar
Ka-Ping Yee committed
277
        try:
278
            doc = formatter(info, self.context)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
279 280
        except:                         # just in case something goes wrong
            import traceback
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:
Ka-Ping Yee's avatar
Ka-Ping Yee committed
294
            import os, tempfile
295
            suffix = ['.txt', '.html'][self.format=="html"]
296
            (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
Ka-Ping Yee's avatar
Ka-Ping Yee committed
297
            try:
298
                file = os.fdopen(fd, 'w')
Ka-Ping Yee's avatar
Ka-Ping Yee committed
299 300
                file.write(doc)
                file.close()
301
                msg = '<p> %s contains the description of this error.' % path
Ka-Ping Yee's avatar
Ka-Ping Yee committed
302
            except:
303 304 305 306 307
                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
308 309

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