00001 """More comprehensive traceback formatting for Python scripts.
00002
00003 To enable this module, do:
00004
00005 import cgitb; cgitb.enable()
00006
00007 at the top of your script. The optional arguments to enable() are:
00008
00009 display - if true, tracebacks are displayed in the web browser
00010 logdir - if set, tracebacks are written to files in this directory
00011 context - number of lines of source code to show for each stack frame
00012 format - 'text' or 'html' controls the output format
00013 viewClass - sub class of View. Create this if you want to customize the
00014 layout of the traceback.
00015 debug - may be used by viewClass to decide on level of detail
00016
00017 By default, tracebacks are displayed but not saved, the context is 5 lines
00018 and the output format is 'html' (for backwards compatibility with the
00019 original use of this module).
00020
00021 Alternatively, if you have caught an exception and want cgitb to display it
00022 for you, call cgitb.handler(). The optional argument to handler() is a
00023 3-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
00024 The default handler displays output as HTML.
00025
00026
00027 2005-04-22 Nir Soffer <nirs@freeshell.org>
00028
00029 Rewrite:
00030 - Refactor html and text functions to View class, HTMLFormatter and
00031 TextFormatter. No more duplicate formating code.
00032 - Layout is done with minimal html and css, in a way it can't be
00033 affected by surrounding code.
00034 - Built to be easy to subclass and modify without duplicating code.
00035 - Change layout, important details come first.
00036 - Factor frame analyzing and formatting into separate class.
00037 - Add debug argument, can be used to change error display e.g. user
00038 error view, developer error view.
00039 - Add viewClass argument, make it easy to customize the traceback view.
00040 - Easy to customize system details and application details.
00041
00042 The main goal of this rewrite was to have a traceback that can render
00043 few tracebacks combined. It's needed when you wrap an expection and want
00044 to print both the traceback up to the wrapper exception, and the
00045 original traceback. There is no code to support this here, but it's easy
00046 to add by using your own View sub class.
00047 """
00048
00049 __author__ = 'Ka-Ping Yee'
00050 __version__ = '$Revision: 1.10 $'
00051
00052 import sys, os, pydoc, inspect, linecache, tokenize, keyword
00053
00054
00055 def reset():
00056 """ Reset the CGI and the browser
00057
00058 Return a string that resets the CGI and browser to a known state.
00059 TODO: probably some of this is not needed any more.
00060 """
00061 return """<!--: spam
00062 Content-Type: text/html
00063
00064 <body><font style="color: white; font-size: 1px"> -->
00065 <body><font style="color: white; font-size: 1px"> --> -->
00066 </font> </font> </font> </script> </object> </blockquote> </pre>
00067 </table> </table> </table> </table> </table> </font> </font> </font>
00068 """
00069
00070 __UNDEF__ = []
00071
00072
00073 class HiddenObject:
00074 def __repr__(self):
00075 return "<HIDDEN>"
00076 HiddenObject = HiddenObject()
00077
00078 class HTMLFormatter:
00079 """ Minimal html formatter """
00080
00081 def attributes(self, attributes=None):
00082 if attributes:
00083 result = [' %s="%s"' % (k, v) for k, v in attributes.items()]
00084 return ''.join(result)
00085 return ''
00086
00087 def tag(self, name, text, attributes=None):
00088 return '<%s%s>%s</%s>\n' % (name, self.attributes(attributes), text, name)
00089
00090 def section(self, text, attributes=None):
00091 return self.tag('div', text, attributes)
00092
00093 def title(self, text, attributes=None):
00094 return self.tag('h1', text, attributes)
00095
00096 def subTitle(self, text, attributes=None):
00097 return self.tag('h2', text, attributes)
00098
00099 def subSubTitle(self, text, attributes=None):
00100 return self.tag('h3', text, attributes)
00101
00102 def paragraph(self, text, attributes=None):
00103 return self.tag('p', text, attributes)
00104
00105 def list(self, items, attributes=None):
00106 return self.formatList('ul', items, attributes)
00107
00108 def orderedList(self, items, attributes=None):
00109 return self.formatList('ol', items, attributes)
00110
00111 def formatList(self, name, items, attributes=None):
00112 """ Send list of raw texts or formatted items. """
00113 if isinstance(items, (list, tuple)):
00114 items = '\n' + ''.join([self.listItem(i) for i in items])
00115 return self.tag(name, items, attributes)
00116
00117 def listItem(self, text, attributes=None):
00118 return self.tag('li', text, attributes)
00119
00120 def link(self, href, text, attributes=None):
00121 if attributes is None:
00122 attributes = {}
00123 attributes['href'] = href
00124 return self.tag('a', text, attributes)
00125
00126 def strong(self, text, attributes=None):
00127 return self.tag('strong', text, attributes)
00128
00129 def em(self, text, attributes=None):
00130 return self.tag('em', text, attributes)
00131
00132 def repr(self, object):
00133 return pydoc.html.repr(object)
00134
00135
00136 class TextFormatter:
00137 """ Plain text formatter """
00138
00139 def section(self, text, attributes=None):
00140 return text
00141
00142 def title(self, text, attributes=None):
00143 lineBellow = '=' * len(text)
00144 return '%s\n%s\n\n' % (text, lineBellow)
00145
00146 def subTitle(self, text, attributes=None):
00147 lineBellow = '-' * len(text)
00148 return '%s\n%s\n\n' % (text, lineBellow)
00149
00150 def subSubTitle(self, text, attributes=None):
00151 lineBellow = '~' * len(text)
00152 return '%s\n%s\n\n' % (text, lineBellow)
00153
00154 def paragraph(self, text, attributes=None):
00155 return text + '\n\n'
00156
00157 def list(self, items, attributes=None):
00158 if isinstance(items, (list, tuple)):
00159 items = [' * %s\n' % i for i in items]
00160 return ''.join(items) + '\n'
00161 return items
00162
00163 def orderedList(self, items, attributes=None):
00164 if isinstance(items, (list, tuple)):
00165 result = []
00166 for i in range(items):
00167 result.append(' %d. %s\n' % (i, items[i]))
00168 return ''.join(result) + '\n'
00169 return items
00170
00171 def listItem(self, text, attributes=None):
00172 return ' * %s\n' % text
00173
00174 def link(self, href, text, attributes=None):
00175 return '[[%s]]' % text
00176
00177 def strong(self, text, attributes=None):
00178 return text
00179
00180 def em(self, text, attributes=None):
00181 return text
00182
00183 def repr(self, object):
00184 return repr(object)
00185
00186
00187 class Frame:
00188 """ Analyze and format single frame in a traceback """
00189
00190 def __init__(self, frame, file, lnum, func, lines, index):
00191 self.frame = frame
00192 self.file = file
00193 self.lnum = lnum
00194 self.func = func
00195 self.lines = lines
00196 self.index = index
00197
00198 def format(self, formatter):
00199 """ Return formatted content """
00200 self.formatter = formatter
00201 vars, highlight = self.scan()
00202 items = [self.formatCall(),
00203 self.formatContext(highlight),
00204 self.formatVariables(vars)]
00205 return ''.join(items)
00206
00207
00208
00209
00210 def formatCall(self):
00211 call = '%s in %s%s' % (self.formatFile(),
00212 self.formatter.strong(self.func),
00213 self.formatArguments(),)
00214 return self.formatter.paragraph(call, {'class': 'call'})
00215
00216 def formatFile(self):
00217 """ Return formatted file link """
00218 if not self.file:
00219 return '?'
00220 file = pydoc.html.escape(os.path.abspath(self.file))
00221 return self.formatter.link('file://' + file, file)
00222
00223 def formatArguments(self):
00224 """ Return formated arguments list """
00225 if self.func == '?':
00226 return ''
00227
00228 def formatValue(value):
00229 return '=' + self.formatter.repr(value)
00230
00231 args, varargs, varkw, locals = inspect.getargvalues(self.frame)
00232 return inspect.formatargvalues(args, varargs, varkw, locals,
00233 formatvalue=formatValue)
00234
00235 def formatContext(self, highlight):
00236 """ Return formatted context, next call highlighted """
00237 if self.index is None:
00238 return ''
00239 context = []
00240 i = self.lnum - self.index
00241 for line in self.lines:
00242 line = '%5d %s' % (i, pydoc.html.escape(line))
00243 attributes = {}
00244 if i in highlight:
00245 attributes = {'class': 'highlight'}
00246 context.append(self.formatter.listItem(line, attributes))
00247 i += 1
00248 context = '\n' + ''.join(context) + '\n'
00249 return self.formatter.orderedList(context, {'class': 'context'})
00250
00251 def formatVariables(self, vars):
00252 """ Return formatted variables """
00253 done = {}
00254 dump = []
00255 for name, where, value in vars:
00256 if name in done:
00257 continue
00258 done[name] = 1
00259 if value is __UNDEF__:
00260 dump.append('%s %s' % (name, self.formatter.em('undefined')))
00261 else:
00262 dump.append(self.formatNameValue(name, where, value))
00263 return self.formatter.list(dump, {'class': 'variables'})
00264
00265 def formatNameValue(self, name, where, value):
00266 """ Format variable name and value according to scope """
00267 if where in ['global', 'builtin']:
00268 name = '%s %s' % (self.formatter.em(where),
00269 self.formatter.strong(name))
00270 elif where == 'local':
00271 name = self.formatter.strong(name)
00272 else:
00273 name = where + self.formatter.strong(name.split('.')[-1])
00274 return '%s = %s' % (name, self.formatter.repr(value))
00275
00276
00277
00278
00279 def scan(self):
00280 """ Scan frame for vars while setting highlight line """
00281 highlight = {}
00282
00283 def reader(lnum=[self.lnum]):
00284 highlight[lnum[0]] = 1
00285 try:
00286 return linecache.getline(self.file, lnum[0])
00287 finally:
00288 lnum[0] += 1
00289
00290 vars = self.scanVariables(reader)
00291 return vars, highlight
00292
00293 def scanVariables(self, reader):
00294 """ Lookup variables in one logical Python line """
00295 vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
00296 for ttype, token, start, end, line in tokenize.generate_tokens(reader):
00297 if ttype == tokenize.NEWLINE:
00298 break
00299 if ttype == tokenize.NAME and token not in keyword.kwlist:
00300 if lasttoken == '.':
00301 if parent is not __UNDEF__:
00302 if self.unsafe_name(token):
00303 value = HiddenObject
00304 else:
00305 value = getattr(parent, token, __UNDEF__)
00306 vars.append((prefix + token, prefix, value))
00307 else:
00308 where, value = self.lookup(token)
00309 vars.append((token, where, value))
00310 elif token == '.':
00311 prefix += lasttoken + '.'
00312 parent = value
00313 else:
00314 parent, prefix = None, ''
00315 lasttoken = token
00316 return vars
00317
00318 def lookup(self, name):
00319 """ Return the scope and the value of name """
00320 scope = None
00321 value = __UNDEF__
00322 locals = inspect.getargvalues(self.frame)[3]
00323 if name in locals:
00324 scope, value = 'local', locals[name]
00325 elif name in self.frame.f_globals:
00326 scope, value = 'global', self.frame.f_globals[name]
00327 elif '__builtins__' in self.frame.f_globals:
00328 scope = 'builtin'
00329 builtins = self.frame.f_globals['__builtins__']
00330 if isinstance(builtins, dict):
00331 value = builtins.get(name, __UNDEF__)
00332 else:
00333 value = getattr(builtins, name, __UNDEF__)
00334 if self.unsafe_name(name):
00335 value = HiddenObject
00336 return scope, value
00337
00338 def unsafe_name(self, name):
00339 return name in self.frame.f_globals.get("unsafe_names", ())
00340
00341 class View:
00342 """ Traceback view """
00343
00344 frameClass = Frame
00345
00346 def __init__(self, info=None, debug=0):
00347 """ Save starting info or current exception info """
00348 self.info = info or sys.exc_info()
00349 self.debug = debug
00350
00351 def format(self, formatter, context=5):
00352 self.formatter = formatter
00353 self.context = context
00354 return formatter.section(self.formatContent(), {'class': 'cgitb'})
00355
00356 def formatContent(self):
00357 """ General layout - override to change layout """
00358 content = (
00359 self.formatStylesheet(),
00360 self.formatTitle(),
00361 self.formatMessage(),
00362 self.formatTraceback(),
00363 self.formatSystemDetails(),
00364 self.formatTextTraceback(),
00365 )
00366 return ''.join(content)
00367
00368
00369
00370
00371 def formatStylesheet(self):
00372 """ Format inline html stylesheet """
00373 return '<style type="text/css">%s</style>' % self.stylesheet()
00374
00375 def stylesheet(self):
00376 """ Return stylesheet rules. Override to change rules.
00377
00378 The rules are sparated to make it easy to extend.
00379
00380 The stylesheet must work even if sorounding code define the same
00381 css names, and it must not change the sorounding code look and
00382 behavior. This is done by having all content in a .traceback
00383 section.
00384 """
00385 return """
00386 .cgitb {background: #E6EAF0; border: 1px solid #4D6180; direction: ltr;}
00387 .cgitb p {margin: 0.5em 0; padding: 5px 10px; text-align: left;}
00388 .cgitb ol {margin: 0}
00389 .cgitb li {margin: 0.25em 0;}
00390 .cgitb h1, .cgitb h2, .cgitb h3 {padding: 5px 10px; margin: 0; background: #4D6180; color: white;}
00391 .cgitb h1 {font-size: 1.3em;}
00392 .cgitb h2 {font-size: 1em; margin-top: 1em;}
00393 .cgitb h3 {font-size: 1em;}
00394 .cgitb .frames {margin: 0; padding: 0; color: #606060}
00395 .cgitb .frames li {display: block;}
00396 .cgitb .call {padding: 5px 10px; background: #A3B4CC; color: black}
00397 .cgitb .context {padding: 0; font-family: monospace; }
00398 .cgitb .context li {display: block; white-space: pre;}
00399 .cgitb .context li.highlight {background: #C0D3F0; color: black}
00400 .cgitb .variables {padding: 5px 10px; font-family: monospace;}
00401 .cgitb .variables li {display: inline;}
00402 .cgitb .variables li:after {content: ", ";}
00403 .cgitb .variables li:last-child:after {content: "";}
00404 .cgitb .exception {border: 1px solid #4D6180; margin: 10px}
00405 .cgitb .exception h3 {background: #4D6180; color: white;}
00406 .cgitb .exception p {color: black;}
00407 .cgitb .exception ul {padding: 0 10px; font-family: monospace;}
00408 .cgitb .exception li {display: block;}
00409 """
00410
00411
00412
00413
00414 def formatTitle(self):
00415 return self.formatter.title(self.exceptionTitle(self.info))
00416
00417 def formatMessage(self):
00418 return self.formatter.paragraph(self.exceptionMessage(self.info))
00419
00420
00421
00422
00423 def formatTraceback(self):
00424 """ Return formatted traceback """
00425 return self.formatOneTraceback(self.info)
00426
00427 def formatOneTraceback(self, info):
00428 """ Format one traceback
00429
00430 Separate to enable formatting multiple tracebacks.
00431 """
00432 output = [self.formatter.subTitle('Traceback'),
00433 self.formatter.paragraph(self.tracebackText(info)),
00434 self.formatter.orderedList(self.tracebackFrames(info),
00435 {'class': 'frames'}),
00436 self.formatter.section(self.formatException(info),
00437 {'class': 'exception'}), ]
00438 return self.formatter.section(''.join(output), {'class': 'traceback'})
00439
00440 def tracebackFrames(self, info):
00441 frames = []
00442 traceback = info[2]
00443 for record in inspect.getinnerframes(traceback, self.context):
00444 frame = self.frameClass(*record)
00445 frames.append(frame.format(self.formatter))
00446 del traceback
00447 return frames
00448
00449 def tracebackText(self, info):
00450 return '''A problem occurred in a Python script. Here is the
00451 sequence of function calls leading up to the error, in the
00452 order they occurred.'''
00453
00454
00455
00456
00457 def formatException(self, info):
00458 items = [self.formatExceptionTitle(info),
00459 self.formatExceptionMessage(info),
00460 self.formatExceptionAttributes(info), ]
00461 return ''.join(items)
00462
00463 def formatExceptionTitle(self, info):
00464 return self.formatter.subSubTitle(self.exceptionTitle(info))
00465
00466 def formatExceptionMessage(self, info):
00467 return self.formatter.paragraph(self.exceptionMessage(info))
00468
00469 def formatExceptionAttributes(self, info):
00470 attribtues = []
00471 for name, value in self.exceptionAttributes(info):
00472 value = self.formatter.repr(value)
00473 attribtues.append('%s = %s' % (name, value))
00474 return self.formatter.list(attribtues)
00475
00476 def exceptionAttributes(self, info):
00477 """ Return list of tuples [(name, value), ...] """
00478 instance = info[1]
00479 attribtues = []
00480 for name in dir(instance):
00481 if name.startswith('_'):
00482 continue
00483 value = getattr(instance, name)
00484 attribtues.append((name, value))
00485 return attribtues
00486
00487 def exceptionTitle(self, info):
00488 type = info[0]
00489 return getattr(type, '__name__', str(type))
00490
00491 def exceptionMessage(self, info):
00492 instance = info[1]
00493 return pydoc.html.escape(str(instance))
00494
00495
00496
00497
00498
00499 def formatSystemDetails(self):
00500 details = ['Date: %s' % self.date(),
00501 'Platform: %s' % self.platform(),
00502 'Python: %s' % self.python(), ]
00503 details += self.applicationDetails()
00504 return (self.formatter.subTitle('System Details') +
00505 self.formatter.list(details, {'class': 'system'}))
00506
00507 def date(self):
00508 import time
00509 rfc2822Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
00510 return rfc2822Date
00511
00512 def platform(self):
00513 try:
00514 return pydoc.html.escape(' '.join(os.uname()))
00515 except:
00516 return pydoc.html.escape('%s (%s)' % (sys.platform, os.name))
00517
00518 def python(self):
00519 return 'Python %s (%s)' % (sys.version.split()[0], sys.executable)
00520
00521 def applicationDetails(self):
00522 """ Override for your application """
00523 return []
00524
00525
00526
00527
00528 def formatTextTraceback(self):
00529 template = self.textTracebackTemplate()
00530 return template % self.formatOneTextTraceback(self.info)
00531
00532 def formatOneTextTraceback(self, info):
00533 """ Separate to enable formatting multiple tracebacks. """
00534 import traceback
00535 return pydoc.html.escape(''.join(traceback.format_exception(*info)))
00536
00537 def textTracebackTemplate(self):
00538 return '''
00539
00540 <!-- The above is a description of an error in a Python program,
00541 formatted for a Web browser. In case you are not reading this
00542 in a Web browser, here is the original traceback:
00543
00544 %s
00545 -->
00546 '''
00547
00548
00549 class Hook:
00550 """A hook to replace sys.excepthook that shows tracebacks in HTML."""
00551
00552 def __init__(self, display=1, logdir=None, context=5, file=None,
00553 format="html", viewClass=View, debug=0):
00554 self.display = display
00555 self.logdir = logdir
00556 self.context = context
00557 self.file = file or sys.stdout
00558 self.format = format
00559 self.viewClass = viewClass
00560 self.debug = debug
00561
00562 def __call__(self, etype, evalue, etb):
00563 self.handle((etype, evalue, etb))
00564
00565 def handle(self, info=None):
00566 info = info or sys.exc_info()
00567 if self.format.lower() == "html":
00568 formatter = HTMLFormatter()
00569 self.file.write(reset())
00570 plain = False
00571 else:
00572 formatter = TextFormatter()
00573 plain = True
00574 try:
00575 view = self.viewClass(info, self.debug)
00576 doc = view.format(formatter, self.context)
00577 except:
00578 raise
00579 import traceback
00580 doc = ''.join(traceback.format_exception(*info))
00581 plain = True
00582
00583 if self.display:
00584 if plain:
00585 doc = doc.replace('&', '&').replace('<', '<')
00586 self.file.write('<pre>' + doc + '</pre>\n')
00587 else:
00588 self.file.write(doc + '\n')
00589 else:
00590 self.file.write('<p>A problem occurred in a Python script.\n')
00591
00592 if self.logdir is not None:
00593 import tempfile
00594 suffix = ['.txt', '.html'][self.format == "html"]
00595 (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
00596 try:
00597 file = os.fdopen(fd, 'w')
00598 file.write(doc)
00599 file.close()
00600 msg = '<p> %s contains the description of this error.' % path
00601 except:
00602 msg = '<p> Tried to save traceback to %s, but failed.' % path
00603 self.file.write(msg + '\n')
00604 try:
00605 self.file.flush()
00606 except: pass
00607
00608
00609 handler = Hook().handle
00610
00611 def enable(display=1, logdir=None, context=5, format="html", viewClass=View, debug=0):
00612 """Install an exception handler that formats tracebacks as HTML.
00613
00614 The optional argument 'display' can be set to 0 to suppress sending the
00615 traceback to the browser, and 'logdir' can be set to a directory to cause
00616 tracebacks to be written to files there."""
00617 sys.excepthook = Hook(display=display, logdir=logdir, context=context,
00618 format=format, viewClass=viewClass, debug=debug)
00619