cgitb.py
Go to the documentation of this file.
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__ = [] # a special sentinel object
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     # Private - formatting
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     # Private - analyzing code
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 # analyze and format a 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     # Stylesheet
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     # Head
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     # Traceback
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     # Exception
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     # System details
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     # Text traceback
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          # send tracebacks to browser if true
00555         self.logdir = logdir            # log tracebacks to files if not None
00556         self.context = context          # number of source code lines per frame
00557         self.file = file or sys.stdout  # place to send the output
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('&', '&amp;').replace('<', '&lt;')
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 


pyclearsilver
Author(s): Scott Hassan/hassan@willowgarage.com
autogenerated on Wed Apr 23 2014 10:35:42