diff options
| author | ianb <devnull@localhost> | 2005-04-22 03:18:20 +0000 |
|---|---|---|
| committer | ianb <devnull@localhost> | 2005-04-22 03:18:20 +0000 |
| commit | 36593a8d40cd1417d2927a78cdbe621090b6e8a5 (patch) | |
| tree | 74dc42b93bb512593869fd57f8be99c198d8c108 /paste/exceptions | |
| parent | 7752a3102fc8aa68ee7e349495bc2e9345c77c00 (diff) | |
| download | paste-36593a8d40cd1417d2927a78cdbe621090b6e8a5.tar.gz | |
Renamed package itself
Diffstat (limited to 'paste/exceptions')
| -rw-r--r-- | paste/exceptions/__init__.py | 1 | ||||
| -rw-r--r-- | paste/exceptions/collector.py | 458 | ||||
| -rw-r--r-- | paste/exceptions/formatter.py | 126 | ||||
| -rw-r--r-- | paste/exceptions/reporter.py | 122 | ||||
| -rw-r--r-- | paste/exceptions/serial_number_generator.py | 114 | ||||
| -rw-r--r-- | paste/exceptions/tests/__init__.py | 1 | ||||
| -rw-r--r-- | paste/exceptions/tests/test_formatter.py | 79 | ||||
| -rw-r--r-- | paste/exceptions/tests/test_reporter.py | 46 |
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 ' <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 + |
