summaryrefslogtreecommitdiff
path: root/paste/exceptions
diff options
context:
space:
mode:
authorianb <devnull@localhost>2005-04-22 03:18:20 +0000
committerianb <devnull@localhost>2005-04-22 03:18:20 +0000
commit36593a8d40cd1417d2927a78cdbe621090b6e8a5 (patch)
tree74dc42b93bb512593869fd57f8be99c198d8c108 /paste/exceptions
parent7752a3102fc8aa68ee7e349495bc2e9345c77c00 (diff)
downloadpaste-36593a8d40cd1417d2927a78cdbe621090b6e8a5.tar.gz
Renamed package itself
Diffstat (limited to 'paste/exceptions')
-rw-r--r--paste/exceptions/__init__.py1
-rw-r--r--paste/exceptions/collector.py458
-rw-r--r--paste/exceptions/formatter.py126
-rw-r--r--paste/exceptions/reporter.py122
-rw-r--r--paste/exceptions/serial_number_generator.py114
-rw-r--r--paste/exceptions/tests/__init__.py1
-rw-r--r--paste/exceptions/tests/test_formatter.py79
-rw-r--r--paste/exceptions/tests/test_reporter.py46
8 files changed, 947 insertions, 0 deletions
diff --git a/paste/exceptions/__init__.py b/paste/exceptions/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/exceptions/__init__.py
@@ -0,0 +1 @@
+#
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)
diff --git a/paste/exceptions/formatter.py b/paste/exceptions/formatter.py
new file mode 100644
index 0000000..e71030d
--- /dev/null
+++ b/paste/exceptions/formatter.py
@@ -0,0 +1,126 @@
+"""
+Formatters for the exception data that comes from ExceptionCollector.
+"""
+
+import cgi
+import serial_number_generator
+
+def html_quote(s):
+ return cgi.escape(s, True)
+
+class AbstractFormatter:
+
+ general_data_order = ['object', 'source_url']
+
+ def __init__(self, show_hidden_frames=False,
+ trim_source_paths=()):
+ self.show_hidden_frames = show_hidden_frames
+ self.trim_source_paths = trim_source_paths
+
+ def format_collected_data(self, exc_data):
+ general_data = {}
+ lines = []
+ show_hidden_frames = self.show_hidden_frames
+ last = exc_data.frames[-1]
+ if last.traceback_hide or last.traceback_stop:
+ # If the last frame was supposed to have been hidden,
+ # there's clearly a problem in the hidden portion of
+ # the framework itself
+ show_hidden_frames = True
+ for frame in exc_data.frames:
+ if frame.traceback_hide and not show_hidden_frames:
+ continue
+ sup = frame.supplement
+ if sup:
+ if sup.object:
+ general_data['object'] = self.format_sup_object(
+ sup.object)
+ if sup.source_url:
+ general_data['source_url'] = self.format_sup_url(
+ sup.source_url)
+ if sup.line:
+ lines.append(self.format_sup_line_pos(self.line, self.column))
+ if sup.expression:
+ lines.append(self.format_sup_expression(sup.expression))
+ if sup.warnings:
+ for warning in sup.warnings:
+ lines.append(self.format_sup_warning(warning))
+ if sup.info:
+ lines.extend(self.format_sup_info(sup.info))
+ filename = frame.filename
+ if filename and self.trim_source_paths:
+ for path, repl in self.trim_source_paths:
+ if filename.startswith(path):
+ filename = repl + filename[len(path):]
+ break
+ lines.append(self.format_source_line(
+ filename or '?',
+ frame.lineno or '?',
+ frame.name or '?'))
+ source = frame.get_source_line()
+ if source:
+ lines.append(self.format_source(source))
+ exc_info = self.format_exception_info(
+ exc_data.exception_type,
+ exc_data.exception_value)
+ general_data = general_data.items()
+ general_data.sort(
+ lambda a, b, self=self:
+ cmp(self.general_data_order.index(a[0]),
+ self.general_data_order.index(b[0])))
+ return self.format_combine(general_data, lines, exc_info)
+
+class TextFormatter(AbstractFormatter):
+
+ def quote(self, s):
+ return s
+ def emphasize(self, s):
+ return s
+ def format_sup_object(self, name):
+ return 'In object: %s' % self.quote(name)
+ def format_sup_url(self, url):
+ return 'URL: %s' % self.quote(url)
+ def format_sup_line_pos(self, line, column):
+ if column:
+ return 'Line %i, Column %i' % (line, column)
+ else:
+ return 'Line %i' % line
+ def format_sup_expression(self, expr):
+ return 'In expression: %s' % self.quote(expr)
+ def format_sup_warning(self, warning):
+ return 'Warning: %s' % self.quote(warning)
+ def format_sup_info(self, info):
+ return [self.quote(info)]
+ def format_source_line(self, filename, lineno, name):
+ return 'File %r, line %s in %s' % (filename, lineno, name)
+ def format_source(self, source_line):
+ return ' ' + self.quote(source_line.strip())
+ def format_exception_info(self, etype, evalue):
+ return self.emphasize(
+ '%s: %s' % (self.quote(etype), self.quote(evalue)))
+ def format_combine(self, general_data, lines, exc_info):
+ lines[:0] = [value for name, value in general_data]
+ lines.append(exc_info)
+ return self.format_combine_lines(lines)
+ def format_combine_lines(self, lines):
+ return '\n'.join(lines)
+
+class HTMLFormatter(TextFormatter):
+
+ def quote(self, s):
+ return html_quote(s)
+ def emphasize(self, s):
+ return '<b>%s</b>' % s
+ def format_sup_url(self, url):
+ return 'URL: <a href="%s">%s</a>' % (url, url)
+ def format_combine_lines(self, lines):
+ return '<br>\n'.join(lines)
+ def format_source_line(self, filename, lineno, name):
+ return 'File %r, line %s in <tt>%s</tt>' % (filename, lineno, name)
+ def format_source(self, source_line):
+ return '&nbsp;&nbsp;<tt>%s</tt>' % self.quote(source_line.strip())
+
+def format_html(exc_data, **ops):
+ return HTMLFormatter(**ops).format_collected_data(exc_data)
+def format_text(exc_data, **ops):
+ return TextFormatter(**ops).format_collected_data(exc_data)
diff --git a/paste/exceptions/reporter.py b/paste/exceptions/reporter.py
new file mode 100644
index 0000000..8568147
--- /dev/null
+++ b/paste/exceptions/reporter.py
@@ -0,0 +1,122 @@
+from email.MIMEText import MIMEText
+from email.MIMEMultipart import MIMEMultipart
+import smtplib
+import time
+import formatter
+
+class Reporter:
+
+ def __init__(self, **conf):
+ for name, value in conf.items():
+ if not hasattr(self, name):
+ raise TypeError(
+ "The keyword argument %s was not expected"
+ % name)
+ setattr(self, name, value)
+ self.check_params()
+
+ def check_params(self):
+ pass
+
+ def format_date(self, exc_data):
+ return time.strftime('%c', exc_data.date)
+
+ def format_html(self, exc_data, **kw):
+ return formatter.format_html(exc_data, **kw)
+
+ def format_text(self, exc_data, **kw):
+ return formatter.format_text(exc_data, **kw)
+
+class EmailReporter(Reporter):
+
+ to_addresses = None
+ from_address = None
+ smtp_server = 'localhost'
+ subject_prefix = ''
+
+ def report(self, exc_data):
+ msg = self.assemble_email(exc_data)
+ server = smtplib.SMTP(self.smtp_server)
+ server.sendmail(self.from_address,
+ self.to_addresses, str(msg))
+ server.quit()
+
+ def check_params(self):
+ if not self.to_addresses:
+ raise ValueError("You must set to_addresses")
+ if not self.from_address:
+ raise ValueError("You must set from_address")
+ if isinstance(self.to_addresses, (str, unicode)):
+ self.to_addresses = [self.to_addresses]
+
+ def assemble_email(self, exc_data):
+ short_html_version = self.format_html(
+ exc_data, show_hidden_frames=False)
+ long_html_version = self.format_html(
+ exc_data, show_hidden_frames=True)
+ text_version = self.format_text(
+ exc_data, show_hidden_frames=False)
+ msg = MIMEMultipart()
+ msg.set_type('multipart/alternative')
+ msg.preamble = msg.epilogue = ''
+ text_msg = MIMEText(text_version)
+ text_msg.set_type('text/plain')
+ text_msg.set_param('charset', 'ASCII')
+ msg.attach(text_msg)
+ html_msg = MIMEText(short_html_version)
+ html_msg.set_type('text/html')
+ # @@: Correct character set?
+ html_msg.set_param('charset', 'UTF-8')
+ html_long = MIMEText(long_html_version)
+ html_long.set_type('text/html')
+ html_long.set_param('charset', 'UTF-8')
+ msg.attach(html_msg)
+ msg.attach(html_long)
+ msg['Subject'] = '%s%s: %s' % (
+ self.subject_prefix, exc_data.exception_type,
+ exc_data.exception_value)
+ msg['From'] = self.from_address
+ msg['To'] = ', '.join(self.to_addresses)
+ return msg
+
+class LogReporter(Reporter):
+
+ filename = None
+ show_hidden_frames = True
+
+ def check_params(self):
+ assert self.filename is not None, (
+ "You must give a filename")
+
+ def report(self, exc_data):
+ text = self.format_text(
+ exc_data, show_hidden_frames=self.show_hidden_frames)
+ f = open(self.filename, 'a')
+ try:
+ f.write(text + '\n' + '-'*60 + '\n')
+ finally:
+ f.close()
+
+class FileReporter(Reporter):
+
+ file = None
+ show_hidden_frames = True
+
+ def check_params(self):
+ assert self.file is not None, (
+ "You must give a file object")
+
+ def report(self, exc_data):
+ text = self.format_text(
+ exc_data, show_hidden_frames=self.show_hidden_frames)
+ print text
+ self.file.write(text + '\n' + '-'*60 + '\n')
+
+class WSGIAppReporter(Reporter):
+
+ def __init__(self, exc_data):
+ self.exc_data = exc_data
+
+ def __call__(self, environ, start_response):
+ start_response('500 Server Error', [('Content-type', 'text/html')])
+ return [formatter.format_html(exc_data)]
diff --git a/paste/exceptions/serial_number_generator.py b/paste/exceptions/serial_number_generator.py
new file mode 100644
index 0000000..0389b3a
--- /dev/null
+++ b/paste/exceptions/serial_number_generator.py
@@ -0,0 +1,114 @@
+"""
+Creates a human-readable identifier, using numbers and digits,
+avoiding ambiguous numbers and letters. hash_identifier can be used
+to create compact representations that are unique for a certain string
+(or concatenation of strings)
+"""
+
+import md5
+
+good_characters = "023456789abcdefghjkmnpqrtuvwxyz"
+
+base = len(good_characters)
+
+def make_identifier(number):
+ """
+ Encodes a number as an identifier.
+ """
+ if not isinstance(number, (int, long)):
+ raise ValueError(
+ "You can only make identifiers out of integers (not %r)"
+ % number)
+ if number < 0:
+ raise ValueError(
+ "You cannot make identifiers out of negative numbers: %r"
+ % number)
+ result = []
+ while number:
+ next = number % base
+ result.append(good_characters[next])
+ # Note, this depends on integer rounding of results:
+ number = number / base
+ return ''.join(result)
+
+def hash_identifier(s, length, pad=True, hasher=md5, prefix='',
+ group=None, upper=False):
+ """
+ Hashes the string (with the given hashing module), then turns that
+ hash into an identifier of the given length (using modulo to
+ reduce the length of the identifier). If ``pad`` is False, then
+ the minimum-length identifier will be used; otherwise the
+ identifier will be padded with 0's as necessary.
+
+ ``prefix`` will be added last, and does not count towards the
+ target length. ``group`` will group the characters with ``-`` in
+ the given lengths, and also does not count towards the target
+ length. E.g., ``group=4`` will cause a identifier like
+ ``a5f3-hgk3-asdf``. Grouping occurs before the prefix.
+ """
+ if length > 26 and hasher is md5:
+ raise ValueError, (
+ "md5 cannot create hashes longer than 26 characters in "
+ "length (you gave %s)" % length)
+ if isinstance(s, unicode):
+ s = s.encode('utf-8')
+ h = hasher.new(str(s))
+ bin_hash = h.digest()
+ modulo = base ** length
+ number = 0
+ for c in list(bin_hash):
+ number = (number * 256 + ord(c)) % modulo
+ ident = make_identifier(number)
+ if pad:
+ ident = good_characters[0]*(length-len(ident)) + ident
+ if group:
+ parts = []
+ while ident:
+ parts.insert(0, ident[-group:])
+ ident = ident[:-group]
+ ident = '-'.join(parts)
+ if upper:
+ ident = ident.upper()
+ return prefix + ident
+
+# doctest tests:
+__test__ = {
+ 'make_identifier': """
+ >>> make_identifier(0)
+ ''
+ >>> make_identifier(1000)
+ '922'
+ >>> make_identifier(-100)
+ Traceback (most recent call last):
+ ...
+ ValueError: You cannot make identifiers out of negative numbers: -100
+ >>> make_identifier('test')
+ Traceback (most recent call last):
+ ...
+ ValueError: You can only make identifiers out of integers (not 'test')
+ >>> make_identifier(1000000000000)
+ '5bqderb62'
+ """,
+ 'hash_identifier': """
+ >>> hash_identifier(0, 5)
+ 'fg35w'
+ >>> hash_identifier(0, 10)
+ 'fg35w4t7yv'
+ >>> hash_identifier('this is a test of a long string', 5)
+ 'qpvbe'
+ >>> hash_identifier(0, 26)
+ 'fg35w4t7yvwr8rxpr3g06xj7cf'
+ >>> hash_identifier(0, 30)
+ Traceback (most recent call last):
+ ...
+ ValueError: md5 cannot create hashes longer than 26 characters in length (you gave 30)
+ >>> hash_identifier(0, 10, group=4)
+ 'fg-35w4-t7yv'
+ >>> hash_identifier(0, 10, group=4, upper=True, prefix='M-')
+ 'M-FG-35W4-T7YV'
+ """}
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
+
diff --git a/paste/exceptions/tests/__init__.py b/paste/exceptions/tests/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/exceptions/tests/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/paste/exceptions/tests/test_formatter.py b/paste/exceptions/tests/test_formatter.py
new file mode 100644
index 0000000..6a9b255
--- /dev/null
+++ b/paste/exceptions/tests/test_formatter.py
@@ -0,0 +1,79 @@
+from paste.exceptions import formatter
+from paste.exceptions import collector
+import sys
+import os
+
+class Mock(object):
+ def __init__(self, **kw):
+ for name, value in kw.items():
+ setattr(self, name, value)
+
+class Supplement(Mock):
+
+ object = 'test_object'
+ source_url = 'http://whatever.com'
+ info = 'This is some supplemental information'
+ args = ()
+ def getInfo(self):
+ return self.info
+
+ def __call__(self, *args):
+ self.args = args
+ return self
+
+class BadSupplement(Supplement):
+
+ def getInfo(self):
+ raise ValueError("This supplemental info is buggy")
+
+def call_error(sup):
+ 1 + 2
+ __traceback_supplement__ = (sup, ())
+ assert 0, "I am an error"
+
+def raise_error(sup='default'):
+ if sup == 'default':
+ sup = Supplement()
+ for i in range(10):
+ __traceback_info__ = i
+ if i == 5:
+ call_error(sup=sup)
+
+def format(type='html', **ops):
+ data = collector.collect_exception(*sys.exc_info())
+ report = getattr(formatter, 'format_' + type)(data, **ops)
+ return report
+
+formats = ('html', 'text')
+
+def test_excersize():
+ for f in formats:
+ try:
+ raise_error()
+ except:
+ format(f)
+
+def test_content():
+ for f in formats:
+ try:
+ raise_error()
+ except:
+ result = format(f)
+ print result
+ assert 'test_object' in result
+ assert 'http://whatever.com' in result
+ assert 'This is some supplemental information' in result
+ assert 'raise_error' in result
+ assert 'call_error' in result
+ assert '5' in result
+ assert 'test_content' in result
+
+def test_trim():
+ current = os.path.abspath(os.getcwd())
+ for f in formats:
+ try:
+ raise_error()
+ except:
+ result = format(f, trim_source_paths=[(current, '.')])
+ assert current not in result
+ assert '/test_formatter.py' in result
diff --git a/paste/exceptions/tests/test_reporter.py b/paste/exceptions/tests/test_reporter.py
new file mode 100644
index 0000000..da6c470
--- /dev/null
+++ b/paste/exceptions/tests/test_reporter.py
@@ -0,0 +1,46 @@
+import sys
+import os
+from paste.exceptions.reporter import *
+from paste.exceptions import collector
+
+def setup_file(fn, content=None):
+ fn = os.path.join(os.path.dirname(__file__), 'reporter_output', fn)
+ if os.path.exists(fn):
+ os.unlink(fn)
+ if content is not None:
+ f = open(fn, 'wb')
+ f.write(content)
+ f.close()
+ return fn
+
+def test_logger():
+ fn = setup_file('test_logger.log')
+ rep = LogReporter(
+ filename=fn,
+ show_hidden_frames=False)
+ try:
+ int('a')
+ except:
+ exc_data = collector.collect_exception(*sys.exc_info())
+ else:
+ assert 0
+ rep.report(exc_data)
+ content = open(fn).read()
+ assert len(content.splitlines()) == 4
+ assert 'ValueError' in content
+ assert 'int(): a' in content
+ assert 'test_reporter.py' in content
+ assert 'test_logger' in content
+
+ try:
+ 1 / 0
+ except:
+ exc_data = collector.collect_exception(*sys.exc_info())
+ else:
+ assert 0
+ rep.report(exc_data)
+ content = open(fn).read()
+ print content
+ assert len(content.splitlines()) == 8
+ assert 'ZeroDivisionError' in content
+