summaryrefslogtreecommitdiff
path: root/paste/exceptions/collector.py
diff options
context:
space:
mode:
Diffstat (limited to 'paste/exceptions/collector.py')
-rw-r--r--paste/exceptions/collector.py458
1 files changed, 458 insertions, 0 deletions
diff --git a/paste/exceptions/collector.py b/paste/exceptions/collector.py
new file mode 100644
index 0000000..7d8eaf3
--- /dev/null
+++ b/paste/exceptions/collector.py
@@ -0,0 +1,458 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+## Originally zExceptions.ExceptionFormatter from Zope;
+## Modified by Ian Bicking, Imaginary Landscape, 2005
+"""
+An exception collector that finds traceback information plus
+supplements
+"""
+
+import sys
+import cgi
+import traceback
+import time
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+import linecache
+import serial_number_generator
+
+DEBUG_EXCEPTION_FORMATTER = True
+DEBUG_IDENT_PREFIX = 'E-'
+
+class ExceptionCollector:
+
+ """
+ Produces a data structure that can be used by formatters to
+ display exception reports.
+
+ Magic variables:
+
+ If you define one of these variables in your local scope, you can
+ add information to tracebacks that happen in that context. This
+ allows applications to add all sorts of extra information about
+ the context of the error, including URLs, environmental variables,
+ users, hostnames, etc. These are the variables we look for:
+
+ ``__traceback_supplement__``:
+ You can define this locally or globally (unlike all the other
+ variables, which must be defined locally).
+
+ ``__traceback_supplement__`` is a tuple of ``(factory, arg1,
+ arg2...)``. When there is an exception, ``factory(arg1, arg2,
+ ...)`` is called, and the resulting object is inspected for
+ supplemental information.
+
+ ``__traceback_info__``:
+ This information is added to the traceback, usually fairly
+ literally.
+
+ ``__traceback_hide__``:
+ If set and true, this indicates that the frame should be
+ hidden from abbreviated tracebacks. This way you can hide
+ some of the complexity of the larger framework and let the
+ user focus on their own errors.
+
+ ``__traceback_stop__``:
+ If set and true, then all frames before this should be hidden
+ (as though they had ``__traceback_hide__`` set). This way
+ you can, for instance, hide an entire server.
+
+ ``__traceback_start__``:
+ Start up the traceback again if ``__traceback_stop__`` has
+ come into play.
+
+ ``__traceback_reporter__``:
+ This should be a reporter object (see the reporter module),
+ or a list/tuple of reporter objects. All reporters found this
+ way will be given the exception, innermost first.
+
+ The actually interpretation of these values is largely up to the
+ reporters and formatters.
+
+ collect_exception(*sys.exc_info()) will return an object with
+ several attributes:
+
+ ``frames``:
+ A list of frames
+ ``exception_formatted``:
+ The formatted exception, generally a full traceback
+ ``exception_type``:
+ The string type of the exception, like ``'ValueError'``
+ ``exception_value``:
+ The string value of the exception, like ``'x not in list'``
+ ``identification_code``:
+ A hash of the exception data meant to identify the general
+ exception, so that it shares this code with other exceptions
+ that derive from the same problem. The code is a hash of
+ all the module names and function names in the traceback,
+ plus exception_type. This should be shown to users so they
+ can refer to the exception later. (@@: should it include a
+ portion that allows identification of the specific instance
+ of the exception as well?)
+
+ The list of frames goes innermost first. Each frame has these
+ attributes; some values may be None if they could not be
+ determined.
+
+ ``modname``:
+ the name of the module
+ ``filename``:
+ the filename of the module
+ ``lineno``:
+ the line of the error
+ ``revision``:
+ the contents of __version__ or __revision__
+ ``name``:
+ the function name
+ ``supplement``:
+ an object created from ``__traceback_supplement__``
+ ``supplement_exception``:
+ a simple traceback of any exception ``__traceback_supplement__``
+ created
+ ``traceback_info``:
+ the str() of any ``__traceback_info__`` variable found in the local
+ scope (@@: should it str()-ify it or not?)
+ ``traceback_hide``:
+ the value of any ``__traceback_hide__`` variable
+ ``traceback_stop``:
+ the value of any ``__traceback_stop__`` variable
+ ``traceback_start``:
+ the value of any ``__traceback_start__`` variable
+ ``traceback_log``:
+ the value of any ``__traceback_log__`` variable
+
+
+ ``__traceback_supplement__`` is thrown away, but a fixed
+ set of attributes are captured; each of these attributes is
+ optional.
+
+ ``object``:
+ the name of the object being visited
+ ``source_url``:
+ the original URL requested
+ ``line``:
+ the line of source being executed (for interpreters, like ZPT)
+ ``column``:
+ the column of source being executed
+ ``expression``:
+ the expression being evaluated (also for interpreters)
+ ``warnings``:
+ a list of (string) warnings to be displayed
+ ``getInfo``:
+ a function/method that takes no arguments, and returns a string
+ describing any extra information
+
+ These are used to create an object with attributes of the same
+ names (``getInfo`` becomes a string attribute, not a method).
+ ``__traceback_supplement__`` implementations should be careful to
+ produce values that are relatively static and unlikely to cause
+ further errors in the reporting system -- any complex
+ introspection should go in ``getInfo()`` and should ultimately
+ return a string.
+
+ Note that all attributes are optional, and under certain
+ circumstances may be None or may not exist at all -- the collector
+ can only do a best effort, but must avoid creating any exceptions
+ itself.
+
+ Formatters may want to use ``__traceback_hide__`` as a hint to
+ hide frames that are part of the 'framework' or underlying system;
+ any frames that precede ``__traceback_stop__`` should be treated
+ similarly. Completely hiding these frames may be confusing, but
+ it allows an abbreviated view of the exception that may highlight
+ problems (it is advised that a complete traceback also be
+ generated). If the last frame has one of these variables set, you
+ should probably ignore the variables entirely, as it means there
+ is an unexpected error in the framework.
+
+ TODO:
+
+ More attributes in __traceback_supplement__? Maybe an attribute
+ that gives a list of local variables that should also be
+ collected? Also, attributes that would be explicitly meant for
+ the entire request, not just a single frame. Right now some of
+ the fixed set of attributes (e.g., source_url) are meant for this
+ use, but there's no explicit way for the supplement to indicate
+ new values, e.g., logged-in user, HTTP referrer, environment, etc.
+ Also, the attributes that do exist are Zope/Web oriented.
+
+ More information on frames? cgitb, for instance, produces
+ extensive information on local variables. There exists the
+ possibility that getting this information may cause side effects,
+ which can make debugging more difficult; but it also provides
+ fodder for post-mortem debugging. However, the collector is not
+ meant to be configurable, but to capture everything it can and let
+ the formatters be configurable. Maybe this would have to be a
+ configuration value, or maybe it could be indicated by another
+ magical variable (which would probably mean 'show all local
+ variables below this frame')
+ """
+
+ show_revisions = 0
+
+ def __init__(self, limit=None):
+ self.limit = limit
+
+ def getLimit(self):
+ limit = self.limit
+ if limit is None:
+ limit = getattr(sys, 'tracebacklimit', None)
+ return limit
+
+ def getRevision(self, globals):
+ if not self.show_revisions:
+ return None
+ revision = globals.get('__revision__', None)
+ if revision is None:
+ # Incorrect but commonly used spelling
+ revision = globals.get('__version__', None)
+
+ if revision is not None:
+ try:
+ revision = str(revision).strip()
+ except:
+ revision = '???'
+ return revision
+
+ def collectSupplement(self, supplement, tb):
+ result = {}
+
+ for name in ('object', 'source_url', 'line', 'column',
+ 'expression', 'warnings'):
+ result[name] = getattr(supplement, name, None)
+
+ func = getattr(supplement, 'getInfo', None)
+ if func:
+ result['info'] = func()
+ else:
+ result['info'] = None
+ return SupplementaryData(**result)
+
+ def collectLine(self, tb):
+ f = tb.tb_frame
+ lineno = tb.tb_lineno
+ co = f.f_code
+ filename = co.co_filename
+ name = co.co_name
+ locals = f.f_locals
+ globals = f.f_globals
+
+ data = {}
+ data['modname'] = globals.get('__name__', None)
+ data['filename'] = filename
+ data['lineno'] = lineno
+ data['revision'] = self.getRevision(globals)
+ data['name'] = name
+
+ # Output a traceback supplement, if any.
+ if locals.has_key('__traceback_supplement__'):
+ # Use the supplement defined in the function.
+ tbs = locals['__traceback_supplement__']
+ elif globals.has_key('__traceback_supplement__'):
+ # Use the supplement defined in the module.
+ # This is used by Scripts (Python).
+ tbs = globals['__traceback_supplement__']
+ else:
+ tbs = None
+ if tbs is not None:
+ factory = tbs[0]
+ args = tbs[1:]
+ try:
+ supp = factory(*args)
+ data['supplement'] = self.collectSupplement(supp, tb)
+ except:
+ if DEBUG_EXCEPTION_FORMATTER:
+ out = StringIO()
+ traceback.print_exc(file=out)
+ text = out.getvalue()
+ data['supplement_exception'] = text
+ # else just swallow the exception.
+
+ try:
+ tbi = locals.get('__traceback_info__', None)
+ if tbi is not None:
+ data['traceback_info'] = str(tbi)
+ except:
+ pass
+
+ marker = []
+ for name in ('__traceback_hide__', '__traceback_stop__',
+ '__traceback_start__', '__traceback_log__'):
+ try:
+ tbh = locals.get(name, marker)
+ if tbh is not marker:
+ data[name[2:-2]] = tbh
+ except:
+ pass
+
+ return data
+
+ def collectExceptionOnly(self, etype, value):
+ return traceback.format_exception_only(etype, value)
+
+ def collectException(self, etype, value, tb, limit=None):
+ # The next line provides a way to detect recursion.
+ __exception_formatter__ = 1
+ frames = []
+ ident_data = []
+ if limit is None:
+ limit = self.getLimit()
+ n = 0
+ while tb is not None and (limit is None or n < limit):
+ if tb.tb_frame.f_locals.get('__exception_formatter__'):
+ # Stop recursion.
+ result.append('(Recursive formatException() stopped)\n')
+ break
+ data = self.collectLine(tb)
+ frame = ExceptionFrame(**data)
+ frames.append(frame)
+ ident_data.append(frame.modname or '?')
+ ident_data.append(frame.name or '?')
+ tb = tb.tb_next
+ n = n + 1
+ ident_data.append(str(etype))
+ ident = serial_number_generator.hash_identifier(
+ ' '.join(ident_data), length=5, upper=True,
+ prefix=DEBUG_IDENT_PREFIX)
+
+ result = CollectedException(
+ frames=frames,
+ exception_formatted=self.collectExceptionOnly(etype, value),
+ exception_type=str(etype),
+ exception_value=str(value),
+ identification_code=ident,
+ date=time.localtime())
+ return result
+
+limit = 200
+
+class Bunch:
+
+ """
+ A generic container
+ """
+
+ def __init__(self, **attrs):
+ for name, value in attrs.items():
+ setattr(self, name, value)
+
+ def __repr__(self):
+ name = '<%s ' % self.__class__.__name__
+ name += ' '.join(['%s=%r' % (name, str(value)[:30])
+ for name, value in self.__dict__.items()
+ if not name.startswith('_')])
+ return name + '>'
+
+class CollectedException(Bunch):
+ """
+ This is the result of collection the exception; it contains copies
+ of data of interest.
+ """
+ # A list of frames (ExceptionFrame instances), innermost last:
+ frames = []
+ # The result of traceback.format_exception_only; this looks
+ # like a normal traceback you'd see in the interactive interpreter
+ exception_formatted = None
+ # The *string* representation of the type of the exception
+ # (@@: should we give the # actual class? -- we can't keep the
+ # actual exception around, but the class should be safe)
+ # Something like 'ValueError'
+ exception_type = None
+ # The string representation of the exception, from ``str(e)``.
+ exception_value = None
+ # An identifier which should more-or-less classify this particular
+ # exception, including where in the code it happened.
+ identification_code = None
+ # The date, as time.localtime() returns:
+ date = None
+
+class SupplementaryData(Bunch):
+ """
+ The result of __traceback_supplement__. We don't keep the
+ supplement object around, for fear of GC problems and whatnot.
+ (@@: Maybe I'm being too superstitious about copying only specific
+ information over)
+ """
+
+ # These attributes are copied from the object, or left as None
+ # if the object doesn't have these attributes:
+ object = None
+ source_url = None
+ line = None
+ column = None
+ expression = None
+ warnings = None
+ # This is the *return value* of supplement.getInfo():
+ info = None
+
+class ExceptionFrame(Bunch):
+ """
+ This represents one frame of the exception. Each frame is a
+ context in the call stack, typically represented by a line
+ number and module name in the traceback.
+ """
+
+ # The name of the module; can be None, especially when the code
+ # isn't associated with a module.
+ modname = None
+ # The filename (@@: when no filename, is it None or '?'?)
+ filename = None
+ # Line number
+ lineno = None
+ # The value of __revision__ or __version__ -- but only if
+ # show_revision = True (by defaut it is false). (@@: Why not
+ # collect this?)
+ revision = None
+ # The name of the function with the error (@@: None or '?' when
+ # unknown?)
+ name = None
+ # A SupplementaryData object, if __traceback_supplement__ was found
+ # (and produced no errors)
+ supplement = None
+ # If accessing __traceback_supplement__ causes any error, the
+ # plain-text traceback is stored here
+ supplement_exception = None
+ # The str() of any __traceback_info__ value found
+ traceback_info = None
+ # The value of __traceback_hide__ and __traceback_stop__ variables:
+ traceback_hide = False
+ traceback_stop = False
+
+ def get_source_line(self):
+ """
+ Return the source of the current line of this frame. You
+ probably want to .strip() it as well, as it is likely to have
+ leading whitespace.
+ """
+ if not self.filename or not self.lineno:
+ return None
+ return linecache.getline(self.filename, self.lineno)
+
+if hasattr(sys, 'tracebacklimit'):
+ limit = min(limit, sys.tracebacklimit)
+
+col = ExceptionCollector()
+
+def collect_exception(t, v, tb, limit=None):
+ """
+ Use like:
+
+ try:
+ blah blah
+ except:
+ exc_data = collect_exception(*sys.exc_info())
+ """
+ return col.collectException(t, v, tb, limit=limit)