Home | History | Annotate | Download | only in Lib
      1 """More comprehensive traceback formatting for Python scripts.
      2 
      3 To enable this module, do:
      4 
      5     import cgitb; cgitb.enable()
      6 
      7 at 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 
     14 By default, tracebacks are displayed but not saved, the context is 5 lines
     15 and the output format is 'html' (for backwards compatibility with the
     16 original use of this module)
     17 
     18 Alternatively, if you have caught an exception and want cgitb to display it
     19 for you, call cgitb.handler().  The optional argument to handler() is a
     20 3-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
     21 The default handler displays output as HTML.
     22 
     23 """
     24 import inspect
     25 import keyword
     26 import linecache
     27 import os
     28 import pydoc
     29 import sys
     30 import tempfile
     31 import time
     32 import tokenize
     33 import traceback
     34 
     35 def reset():
     36     """Return a string that resets the CGI and browser to a known state."""
     37     return '''<!--: spam
     38 Content-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
     46 def small(text):
     47     if text:
     48         return '<small>' + text + '</small>'
     49     else:
     50         return ''
     51 
     52 def strong(text):
     53     if text:
     54         return '<strong>' + text + '</strong>'
     55     else:
     56         return ''
     57 
     58 def grey(text):
     59     if text:
     60         return '<font color="#909090">' + text + '</font>'
     61     else:
     62         return ''
     63 
     64 def 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 
     80 def 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 
    101 def 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
    113 function 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 
    191 def 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) + '''
    199 A problem occurred in a Python script.  Here is the sequence of
    200 function 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 
    250 The above is a description of an error in a Python program.  Here is
    251 the original traceback:
    252 
    253 %s
    254 ''' % ''.join(traceback.format_exception(etype, evalue, etb))
    255 
    256 class 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 
    311 handler = Hook().handle
    312 def 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