diff options
author | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 18:52:36 -0800 |
---|---|---|
committer | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 18:52:36 -0800 |
commit | cc83e06efff71b81ca5a3ac6df65775971181295 (patch) | |
tree | d52fa3f1a93730f263c2c5ac8266de8e5fb12abf /paste/exceptions | |
download | paste-git-tox_coverage.tar.gz |
tox.ini: Measure test coveragetox_coverage
Diffstat (limited to 'paste/exceptions')
-rw-r--r-- | paste/exceptions/__init__.py | 6 | ||||
-rw-r--r-- | paste/exceptions/collector.py | 523 | ||||
-rw-r--r-- | paste/exceptions/errormiddleware.py | 466 | ||||
-rw-r--r-- | paste/exceptions/formatter.py | 565 | ||||
-rw-r--r-- | paste/exceptions/reporter.py | 141 | ||||
-rw-r--r-- | paste/exceptions/serial_number_generator.py | 129 |
6 files changed, 1830 insertions, 0 deletions
diff --git a/paste/exceptions/__init__.py b/paste/exceptions/__init__.py new file mode 100644 index 0000000..813f855 --- /dev/null +++ b/paste/exceptions/__init__.py @@ -0,0 +1,6 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Package for catching exceptions and displaying annotated exception +reports +""" diff --git a/paste/exceptions/collector.py b/paste/exceptions/collector.py new file mode 100644 index 0000000..632ce06 --- /dev/null +++ b/paste/exceptions/collector.py @@ -0,0 +1,523 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +############################################################################## +# +# 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 traceback +import time +from six.moves import cStringIO as StringIO +import linecache +from paste.exceptions import serial_number_generator +import warnings + +DEBUG_EXCEPTION_FORMATTER = True +DEBUG_IDENT_PREFIX = 'E-' +FALLBACK_ENCODING = 'UTF-8' + +__all__ = ['collect_exception', 'ExceptionCollector'] + +class ExceptionCollector(object): + + """ + 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. + + By setting it to ``'before'``, all frames before this one will + be thrown away. By setting it to ``'after'`` then all frames + after this will be thrown away until ``'reset'`` is found. In + each case the frame where it is set is included, unless you + append ``'_and_this'`` to the value (e.g., + ``'before_and_this'``). + + Note that formatters will ignore this entirely if the frame + that contains the error wouldn't normally be shown according + to these rules. + + ``__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. + + ``__traceback_decorator__``: + This object (defined in a local or global scope) will get the + result of this function (the CollectedException defined + below). It may modify this object in place, or return an + entirely new object. This gives the object the ability to + manipulate the traceback arbitrarily. + + 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 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_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 + ``extraData``: + a function/method that takes no arguments, and returns a + dictionary. The contents of this dictionary will not be + displayed in the context of the traceback, but globally for + the exception. Results will be grouped by the keys in the + dictionaries (which also serve as titles). The keys can also + be tuples of (importance, title); in this case the importance + should be ``important`` (shows up at top), ``normal`` (shows + up somewhere; unspecified), ``supplemental`` (shows up at + bottom), or ``extra`` (shows up hidden or not at all). + + 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. + There are a variety of rules about special values for this + variables that formatters should be aware of. + + 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 + func = getattr(supplement, 'extraData', None) + if func: + result['extra'] = func() + else: + result['extra'] = None + return SupplementaryData(**result) + + def collectLine(self, tb, extra_data): + f = tb.tb_frame + lineno = tb.tb_lineno + co = f.f_code + filename = co.co_filename + name = co.co_name + globals = f.f_globals + locals = f.f_locals + if not hasattr(locals, 'keys'): + # Something weird about this frame; it's not a real dict + warnings.warn( + "Frame %s has an invalid locals(): %r" % ( + globals.get('__name__', 'unknown'), locals)) + locals = {} + data = {} + data['modname'] = globals.get('__name__', None) + data['filename'] = filename + data['lineno'] = lineno + data['revision'] = self.getRevision(globals) + data['name'] = name + data['tbid'] = id(tb) + + # Output a traceback supplement, if any. + if '__traceback_supplement__' in locals: + # Use the supplement defined in the function. + tbs = locals['__traceback_supplement__'] + elif '__traceback_supplement__' in globals: + # 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) + if data['supplement'].extra: + for key, value in data['supplement'].extra.items(): + extra_data.setdefault(key, []).append(value) + 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_log__', + '__traceback_decorator__'): + try: + tbh = locals.get(name, globals.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 = [] + traceback_decorators = [] + if limit is None: + limit = self.getLimit() + n = 0 + extra_data = {} + while tb is not None and (limit is None or n < limit): + if tb.tb_frame.f_locals.get('__exception_formatter__'): + # Stop recursion. @@: should make a fake ExceptionFrame + frames.append('(Recursive formatException() stopped)\n') + break + data = self.collectLine(tb, extra_data) + frame = ExceptionFrame(**data) + frames.append(frame) + if frame.traceback_decorator is not None: + traceback_decorators.append(frame.traceback_decorator) + 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=etype, + exception_value=self.safeStr(value), + identification_code=ident, + date=time.localtime(), + extra_data=extra_data) + if etype is ImportError: + extra_data[('important', 'sys.path')] = [sys.path] + for decorator in traceback_decorators: + try: + new_result = decorator(result) + if new_result is not None: + result = new_result + except: + pass + return result + + def safeStr(self, obj): + try: + return str(obj) + except UnicodeEncodeError: + try: + return unicode(obj).encode(FALLBACK_ENCODING, 'replace') + except UnicodeEncodeError: + # This is when something is really messed up, but this can + # happen when the __str__ of an object has to handle unicode + return repr(obj) + +limit = 200 + +class Bunch(object): + + """ + 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 + # A dictionary of supplemental data: + extra_data = {} + +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__ + traceback_hide = False + # The value of __traceback_decorator__ + traceback_decorator = None + # The id() of the traceback scope, can be used to reference the + # scope for use elsewhere + tbid = None + + def get_source_line(self, context=0): + """ + 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 context is given, then that many lines on either side will + also be returned. E.g., context=1 will give 3 lines. + """ + if not self.filename or not self.lineno: + return None + lines = [] + for lineno in range(self.lineno-context, self.lineno+context+1): + lines.append(linecache.getline(self.filename, lineno)) + return ''.join(lines) + +if hasattr(sys, 'tracebacklimit'): + limit = min(limit, sys.tracebacklimit) + +col = ExceptionCollector() + +def collect_exception(t, v, tb, limit=None): + """ + Collection an exception from ``sys.exc_info()``. + + 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/errormiddleware.py b/paste/exceptions/errormiddleware.py new file mode 100644 index 0000000..95c1261 --- /dev/null +++ b/paste/exceptions/errormiddleware.py @@ -0,0 +1,466 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Error handler middleware +""" +import sys +import traceback +import cgi +from six.moves import cStringIO as StringIO +from paste.exceptions import formatter, collector, reporter +from paste import wsgilib +from paste import request +import six + +__all__ = ['ErrorMiddleware', 'handle_exception'] + +class _NoDefault(object): + def __repr__(self): + return '<NoDefault>' +NoDefault = _NoDefault() + +class ErrorMiddleware(object): + + """ + Error handling middleware + + Usage:: + + error_catching_wsgi_app = ErrorMiddleware(wsgi_app) + + Settings: + + ``debug``: + If true, then tracebacks will be shown in the browser. + + ``error_email``: + an email address (or list of addresses) to send exception + reports to + + ``error_log``: + a filename to append tracebacks to + + ``show_exceptions_in_wsgi_errors``: + If true, then errors will be printed to ``wsgi.errors`` + (frequently a server error log, or stderr). + + ``from_address``, ``smtp_server``, ``error_subject_prefix``, ``smtp_username``, ``smtp_password``, ``smtp_use_tls``: + variables to control the emailed exception reports + + ``error_message``: + When debug mode is off, the error message to show to users. + + ``xmlhttp_key``: + When this key (default ``_``) is in the request GET variables + (not POST!), expect that this is an XMLHttpRequest, and the + response should be more minimal; it should not be a complete + HTML page. + + Environment Configuration: + + ``paste.throw_errors``: + If this setting in the request environment is true, then this + middleware is disabled. This can be useful in a testing situation + where you don't want errors to be caught and transformed. + + ``paste.expected_exceptions``: + When this middleware encounters an exception listed in this + environment variable and when the ``start_response`` has not + yet occurred, the exception will be re-raised instead of being + caught. This should generally be set by middleware that may + (but probably shouldn't be) installed above this middleware, + and wants to get certain exceptions. Exceptions raised after + ``start_response`` have been called are always caught since + by definition they are no longer expected. + + """ + + def __init__(self, application, global_conf=None, + debug=NoDefault, + error_email=None, + error_log=None, + show_exceptions_in_wsgi_errors=NoDefault, + from_address=None, + smtp_server=None, + smtp_username=None, + smtp_password=None, + smtp_use_tls=False, + error_subject_prefix=None, + error_message=None, + xmlhttp_key=None): + from paste.util import converters + self.application = application + # @@: global_conf should be handled elsewhere in a separate + # function for the entry point + if global_conf is None: + global_conf = {} + if debug is NoDefault: + debug = converters.asbool(global_conf.get('debug')) + if show_exceptions_in_wsgi_errors is NoDefault: + show_exceptions_in_wsgi_errors = converters.asbool(global_conf.get('show_exceptions_in_wsgi_errors')) + self.debug_mode = converters.asbool(debug) + if error_email is None: + error_email = (global_conf.get('error_email') + or global_conf.get('admin_email') + or global_conf.get('webmaster_email') + or global_conf.get('sysadmin_email')) + self.error_email = converters.aslist(error_email) + self.error_log = error_log + self.show_exceptions_in_wsgi_errors = show_exceptions_in_wsgi_errors + if from_address is None: + from_address = global_conf.get('error_from_address', 'errors@localhost') + self.from_address = from_address + if smtp_server is None: + smtp_server = global_conf.get('smtp_server', 'localhost') + self.smtp_server = smtp_server + self.smtp_username = smtp_username or global_conf.get('smtp_username') + self.smtp_password = smtp_password or global_conf.get('smtp_password') + self.smtp_use_tls = smtp_use_tls or converters.asbool(global_conf.get('smtp_use_tls')) + self.error_subject_prefix = error_subject_prefix or '' + if error_message is None: + error_message = global_conf.get('error_message') + self.error_message = error_message + if xmlhttp_key is None: + xmlhttp_key = global_conf.get('xmlhttp_key', '_') + self.xmlhttp_key = xmlhttp_key + + def __call__(self, environ, start_response): + """ + The WSGI application interface. + """ + # We want to be careful about not sending headers twice, + # and the content type that the app has committed to (if there + # is an exception in the iterator body of the response) + if environ.get('paste.throw_errors'): + return self.application(environ, start_response) + environ['paste.throw_errors'] = True + + try: + __traceback_supplement__ = Supplement, self, environ + sr_checker = ResponseStartChecker(start_response) + app_iter = self.application(environ, sr_checker) + return self.make_catching_iter(app_iter, environ, sr_checker) + except: + exc_info = sys.exc_info() + try: + for expect in environ.get('paste.expected_exceptions', []): + if isinstance(exc_info[1], expect): + raise + start_response('500 Internal Server Error', + [('content-type', 'text/html')], + exc_info) + # @@: it would be nice to deal with bad content types here + response = self.exception_handler(exc_info, environ) + if six.PY3: + response = response.encode('utf8') + return [response] + finally: + # clean up locals... + exc_info = None + + def make_catching_iter(self, app_iter, environ, sr_checker): + if isinstance(app_iter, (list, tuple)): + # These don't raise + return app_iter + return CatchingIter(app_iter, environ, sr_checker, self) + + def exception_handler(self, exc_info, environ): + simple_html_error = False + if self.xmlhttp_key: + get_vars = request.parse_querystring(environ) + if dict(get_vars).get(self.xmlhttp_key): + simple_html_error = True + return handle_exception( + exc_info, environ['wsgi.errors'], + html=True, + debug_mode=self.debug_mode, + error_email=self.error_email, + error_log=self.error_log, + show_exceptions_in_wsgi_errors=self.show_exceptions_in_wsgi_errors, + error_email_from=self.from_address, + smtp_server=self.smtp_server, + smtp_username=self.smtp_username, + smtp_password=self.smtp_password, + smtp_use_tls=self.smtp_use_tls, + error_subject_prefix=self.error_subject_prefix, + error_message=self.error_message, + simple_html_error=simple_html_error) + +class ResponseStartChecker(object): + def __init__(self, start_response): + self.start_response = start_response + self.response_started = False + + def __call__(self, *args): + self.response_started = True + self.start_response(*args) + +class CatchingIter(object): + + """ + A wrapper around the application iterator that will catch + exceptions raised by the a generator, or by the close method, and + display or report as necessary. + """ + + def __init__(self, app_iter, environ, start_checker, error_middleware): + self.app_iterable = app_iter + self.app_iterator = iter(app_iter) + self.environ = environ + self.start_checker = start_checker + self.error_middleware = error_middleware + self.closed = False + + def __iter__(self): + return self + + def next(self): + __traceback_supplement__ = ( + Supplement, self.error_middleware, self.environ) + if self.closed: + raise StopIteration + try: + return self.app_iterator.next() + except StopIteration: + self.closed = True + close_response = self._close() + if close_response is not None: + return close_response + else: + raise StopIteration + except: + self.closed = True + close_response = self._close() + exc_info = sys.exc_info() + response = self.error_middleware.exception_handler( + exc_info, self.environ) + if close_response is not None: + response += ( + '<hr noshade>Error in .close():<br>%s' + % close_response) + + if not self.start_checker.response_started: + self.start_checker('500 Internal Server Error', + [('content-type', 'text/html')], + exc_info) + + if six.PY3: + response = response.encode('utf8') + return response + __next__ = next + + def close(self): + # This should at least print something to stderr if the + # close method fails at this point + if not self.closed: + self._close() + + def _close(self): + """Close and return any error message""" + if not hasattr(self.app_iterable, 'close'): + return None + try: + self.app_iterable.close() + return None + except: + close_response = self.error_middleware.exception_handler( + sys.exc_info(), self.environ) + return close_response + + +class Supplement(object): + + """ + This is a supplement used to display standard WSGI information in + the traceback. + """ + + def __init__(self, middleware, environ): + self.middleware = middleware + self.environ = environ + self.source_url = request.construct_url(environ) + + def extraData(self): + data = {} + cgi_vars = data[('extra', 'CGI Variables')] = {} + wsgi_vars = data[('extra', 'WSGI Variables')] = {} + hide_vars = ['paste.config', 'wsgi.errors', 'wsgi.input', + 'wsgi.multithread', 'wsgi.multiprocess', + 'wsgi.run_once', 'wsgi.version', + 'wsgi.url_scheme'] + for name, value in self.environ.items(): + if name.upper() == name: + if value: + cgi_vars[name] = value + elif name not in hide_vars: + wsgi_vars[name] = value + if self.environ['wsgi.version'] != (1, 0): + wsgi_vars['wsgi.version'] = self.environ['wsgi.version'] + proc_desc = tuple([int(bool(self.environ[key])) + for key in ('wsgi.multiprocess', + 'wsgi.multithread', + 'wsgi.run_once')]) + wsgi_vars['wsgi process'] = self.process_combos[proc_desc] + wsgi_vars['application'] = self.middleware.application + if 'paste.config' in self.environ: + data[('extra', 'Configuration')] = dict(self.environ['paste.config']) + return data + + process_combos = { + # multiprocess, multithread, run_once + (0, 0, 0): 'Non-concurrent server', + (0, 1, 0): 'Multithreaded', + (1, 0, 0): 'Multiprocess', + (1, 1, 0): 'Multi process AND threads (?)', + (0, 0, 1): 'Non-concurrent CGI', + (0, 1, 1): 'Multithread CGI (?)', + (1, 0, 1): 'CGI', + (1, 1, 1): 'Multi thread/process CGI (?)', + } + +def handle_exception(exc_info, error_stream, html=True, + debug_mode=False, + error_email=None, + error_log=None, + show_exceptions_in_wsgi_errors=False, + error_email_from='errors@localhost', + smtp_server='localhost', + smtp_username=None, + smtp_password=None, + smtp_use_tls=False, + error_subject_prefix='', + error_message=None, + simple_html_error=False, + ): + """ + For exception handling outside of a web context + + Use like:: + + import sys + from paste.exceptions.errormiddleware import handle_exception + try: + do stuff + except: + handle_exception( + sys.exc_info(), sys.stderr, html=False, ...other config...) + + If you want to report, but not fully catch the exception, call + ``raise`` after ``handle_exception``, which (when given no argument) + will reraise the exception. + """ + reported = False + exc_data = collector.collect_exception(*exc_info) + extra_data = '' + if error_email: + rep = reporter.EmailReporter( + to_addresses=error_email, + from_address=error_email_from, + smtp_server=smtp_server, + smtp_username=smtp_username, + smtp_password=smtp_password, + smtp_use_tls=smtp_use_tls, + subject_prefix=error_subject_prefix) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + if error_log: + rep = reporter.LogReporter( + filename=error_log) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + if show_exceptions_in_wsgi_errors: + rep = reporter.FileReporter( + file=error_stream) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + else: + line = ('Error - %s: %s\n' + % (exc_data.exception_type, exc_data.exception_value)) + if six.PY3: + line = line.encode('utf8') + error_stream.write(line) + if html: + if debug_mode and simple_html_error: + return_error = formatter.format_html( + exc_data, include_hidden_frames=False, + include_reusable=False, show_extra_data=False) + reported = True + elif debug_mode and not simple_html_error: + error_html = formatter.format_html( + exc_data, + include_hidden_frames=True, + include_reusable=False) + head_html = formatter.error_css + formatter.hide_display_js + return_error = error_template( + head_html, error_html, extra_data) + extra_data = '' + reported = True + else: + msg = error_message or ''' + An error occurred. See the error logs for more information. + (Turn debug on to display exception reports here) + ''' + return_error = error_template('', msg, '') + else: + return_error = None + if not reported and error_stream: + err_report = formatter.format_text(exc_data, show_hidden_frames=True) + err_report += '\n' + '-'*60 + '\n' + error_stream.write(err_report) + if extra_data: + error_stream.write(extra_data) + return return_error + +def send_report(rep, exc_data, html=True): + try: + rep.report(exc_data) + except: + output = StringIO() + traceback.print_exc(file=output) + if html: + return """ + <p>Additionally an error occurred while sending the %s report: + + <pre>%s</pre> + </p>""" % ( + cgi.escape(str(rep)), output.getvalue()) + else: + return ( + "Additionally an error occurred while sending the " + "%s report:\n%s" % (str(rep), output.getvalue())) + else: + return '' + +def error_template(head_html, exception, extra): + return ''' + <html> + <head> + <title>Server Error</title> + %s + </head> + <body> + <h1>Server Error</h1> + %s + %s + </body> + </html>''' % (head_html, exception, extra) + +def make_error_middleware(app, global_conf, **kw): + return ErrorMiddleware(app, global_conf=global_conf, **kw) + +doc_lines = ErrorMiddleware.__doc__.splitlines(True) +for i in range(len(doc_lines)): + if doc_lines[i].strip().startswith('Settings'): + make_error_middleware.__doc__ = ''.join(doc_lines[i:]) + break +del i, doc_lines diff --git a/paste/exceptions/formatter.py b/paste/exceptions/formatter.py new file mode 100644 index 0000000..09309de --- /dev/null +++ b/paste/exceptions/formatter.py @@ -0,0 +1,565 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Formatters for the exception data that comes from ExceptionCollector. +""" +# @@: TODO: +# Use this: http://www.zope.org/Members/tino/VisualTraceback/VisualTracebackNews + +import cgi +import six +import re +from paste.util import PySourceColor + +def html_quote(s): + return cgi.escape(str(s), True) + +class AbstractFormatter(object): + + general_data_order = ['object', 'source_url'] + + def __init__(self, show_hidden_frames=False, + include_reusable=True, + show_extra_data=True, + trim_source_paths=()): + self.show_hidden_frames = show_hidden_frames + self.trim_source_paths = trim_source_paths + self.include_reusable = include_reusable + self.show_extra_data = show_extra_data + + def format_collected_data(self, exc_data): + general_data = {} + if self.show_extra_data: + for name, value_list in exc_data.extra_data.items(): + if isinstance(name, tuple): + importance, title = name + else: + importance, title = 'normal', name + for value in value_list: + general_data[(importance, name)] = self.format_extra_data( + importance, title, value) + lines = [] + frames = self.filter_frames(exc_data.frames) + for frame in frames: + sup = frame.supplement + if sup: + if sup.object: + general_data[('important', 'object')] = self.format_sup_object( + sup.object) + if sup.source_url: + general_data[('important', 'source_url')] = self.format_sup_url( + sup.source_url) + if sup.line: + lines.append(self.format_sup_line_pos(sup.line, sup.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)) + if frame.supplement_exception: + lines.append('Exception in supplement:') + lines.append(self.quote_long(frame.supplement_exception)) + if frame.traceback_info: + lines.append(self.format_traceback_info(frame.traceback_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)) + source = frame.get_source_line() + long_source = frame.get_source_line(2) + if source: + lines.append(self.format_long_source( + source, long_source)) + etype = exc_data.exception_type + if not isinstance(etype, six.string_types): + etype = etype.__name__ + exc_info = self.format_exception_info( + etype, + exc_data.exception_value) + data_by_importance = {'important': [], 'normal': [], + 'supplemental': [], 'extra': []} + for (importance, name), value in general_data.items(): + data_by_importance[importance].append( + (name, value)) + for value in data_by_importance.values(): + value.sort() + return self.format_combine(data_by_importance, lines, exc_info) + + def filter_frames(self, frames): + """ + Removes any frames that should be hidden, according to the + values of traceback_hide, self.show_hidden_frames, and the + hidden status of the final frame. + """ + if self.show_hidden_frames: + return frames + new_frames = [] + hidden = False + for frame in frames: + hide = frame.traceback_hide + # @@: It would be nice to signal a warning if an unknown + # hide string was used, but I'm not sure where to put + # that warning. + if hide == 'before': + new_frames = [] + hidden = False + elif hide == 'before_and_this': + new_frames = [] + hidden = False + continue + elif hide == 'reset': + hidden = False + elif hide == 'reset_and_this': + hidden = False + continue + elif hide == 'after': + hidden = True + elif hide == 'after_and_this': + hidden = True + continue + elif hide: + continue + elif hidden: + continue + new_frames.append(frame) + if frames[-1] not in new_frames: + # We must include the last frame; that we don't indicates + # that the error happened where something was "hidden", + # so we just have to show everything + return frames + return new_frames + + def pretty_string_repr(self, s): + """ + Formats the string as a triple-quoted string when it contains + newlines. + """ + if '\n' in s: + s = repr(s) + s = s[0]*3 + s[1:-1] + s[-1]*3 + s = s.replace('\\n', '\n') + return s + else: + return repr(s) + + def long_item_list(self, lst): + """ + Returns true if the list contains items that are long, and should + be more nicely formatted. + """ + how_many = 0 + for item in lst: + if len(repr(item)) > 40: + how_many += 1 + if how_many >= 3: + return True + return False + +class TextFormatter(AbstractFormatter): + + def quote(self, s): + return s + def quote_long(self, s): + return s + def emphasize(self, s): + return s + def format_sup_object(self, obj): + return 'In object: %s' % self.emphasize(self.quote(repr(obj))) + def format_sup_url(self, url): + return 'URL: %s' % self.quote(url) + def format_sup_line_pos(self, line, column): + if column: + return self.emphasize('Line %i, Column %i' % (line, column)) + else: + return self.emphasize('Line %i' % line) + def format_sup_expression(self, expr): + return self.emphasize('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_long(info)] + def format_source_line(self, filename, frame): + return 'File %r, line %s in %s' % ( + filename, frame.lineno or '?', frame.name or '?') + def format_long_source(self, source, long_source): + return self.format_source(source) + 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_traceback_info(self, info): + return info + + def format_combine(self, data_by_importance, lines, exc_info): + lines[:0] = [value for n, value in data_by_importance['important']] + lines.append(exc_info) + for name in 'normal', 'supplemental', 'extra': + lines.extend([value for n, value in data_by_importance[name]]) + return self.format_combine_lines(lines) + + def format_combine_lines(self, lines): + return '\n'.join(lines) + + def format_extra_data(self, importance, title, value): + if isinstance(value, str): + s = self.pretty_string_repr(value) + if '\n' in s: + return '%s:\n%s' % (title, s) + else: + return '%s: %s' % (title, s) + elif isinstance(value, dict): + lines = ['\n', title, '-'*len(title)] + items = value.items() + items = sorted(items) + for n, v in items: + try: + v = repr(v) + except Exception as e: + v = 'Cannot display: %s' % e + v = truncate(v) + lines.append(' %s: %s' % (n, v)) + return '\n'.join(lines) + elif (isinstance(value, (list, tuple)) + and self.long_item_list(value)): + parts = [truncate(repr(v)) for v in value] + return '%s: [\n %s]' % ( + title, ',\n '.join(parts)) + else: + return '%s: %s' % (title, truncate(repr(value))) + +class HTMLFormatter(TextFormatter): + + def quote(self, s): + return html_quote(s) + def quote_long(self, s): + return '<pre>%s</pre>' % self.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, frame): + name = self.quote(frame.name or '?') + return 'Module <span class="module" title="%s">%s</span>:<b>%s</b> in <code>%s</code>' % ( + filename, frame.modname or '?', frame.lineno or '?', + name) + return 'File %r, line %s in <tt>%s</tt>' % ( + filename, frame.lineno, name) + def format_long_source(self, source, long_source): + q_long_source = str2html(long_source, False, 4, True) + q_source = str2html(source, True, 0, False) + return ('<code style="display: none" class="source" source-type="long"><a class="switch_source" onclick="return switch_source(this, \'long\')" href="#"><< </a>%s</code>' + '<code class="source" source-type="short"><a onclick="return switch_source(this, \'short\')" class="switch_source" href="#">>> </a>%s</code>' + % (q_long_source, + q_source)) + def format_source(self, source_line): + return ' <code class="source">%s</code>' % self.quote(source_line.strip()) + def format_traceback_info(self, info): + return '<pre>%s</pre>' % self.quote(info) + + def format_extra_data(self, importance, title, value): + if isinstance(value, str): + s = self.pretty_string_repr(value) + if '\n' in s: + return '%s:<br><pre>%s</pre>' % (title, self.quote(s)) + else: + return '%s: <tt>%s</tt>' % (title, self.quote(s)) + elif isinstance(value, dict): + return self.zebra_table(title, value) + elif (isinstance(value, (list, tuple)) + and self.long_item_list(value)): + return '%s: <tt>[<br>\n %s]</tt>' % ( + title, ',<br> '.join(map(self.quote, map(repr, value)))) + else: + return '%s: <tt>%s</tt>' % (title, self.quote(repr(value))) + + def format_combine(self, data_by_importance, lines, exc_info): + lines[:0] = [value for n, value in data_by_importance['important']] + lines.append(exc_info) + for name in 'normal', 'supplemental': + lines.extend([value for n, value in data_by_importance[name]]) + if data_by_importance['extra']: + lines.append( + '<script type="text/javascript">\nshow_button(\'extra_data\', \'extra data\');\n</script>\n' + + '<div id="extra_data" class="hidden-data">\n') + lines.extend([value for n, value in data_by_importance['extra']]) + lines.append('</div>') + text = self.format_combine_lines(lines) + if self.include_reusable: + return error_css + hide_display_js + text + else: + # Usually because another error is already on this page, + # and so the js & CSS are unneeded + return text + + def zebra_table(self, title, rows, table_class="variables"): + if isinstance(rows, dict): + rows = rows.items() + rows = sorted(rows) + table = ['<table class="%s">' % table_class, + '<tr class="header"><th colspan="2">%s</th></tr>' + % self.quote(title)] + odd = False + for name, value in rows: + try: + value = repr(value) + except Exception as e: + value = 'Cannot print: %s' % e + odd = not odd + table.append( + '<tr class="%s"><td>%s</td>' + % (odd and 'odd' or 'even', self.quote(name))) + table.append( + '<td><tt>%s</tt></td></tr>' + % make_wrappable(self.quote(truncate(value)))) + table.append('</table>') + return '\n'.join(table) + +hide_display_js = r''' +<script type="text/javascript"> +function hide_display(id) { + var el = document.getElementById(id); + if (el.className == "hidden-data") { + el.className = ""; + return true; + } else { + el.className = "hidden-data"; + return false; + } +} +document.write('<style type="text/css">\n'); +document.write('.hidden-data {display: none}\n'); +document.write('</style>\n'); +function show_button(toggle_id, name) { + document.write('<a href="#' + toggle_id + + '" onclick="javascript:hide_display(\'' + toggle_id + + '\')" class="button">' + name + '</a><br>'); +} + +function switch_source(el, hide_type) { + while (el) { + if (el.getAttribute && + el.getAttribute('source-type') == hide_type) { + break; + } + el = el.parentNode; + } + if (! el) { + return false; + } + el.style.display = 'none'; + if (hide_type == 'long') { + while (el) { + if (el.getAttribute && + el.getAttribute('source-type') == 'short') { + break; + } + el = el.nextSibling; + } + } else { + while (el) { + if (el.getAttribute && + el.getAttribute('source-type') == 'long') { + break; + } + el = el.previousSibling; + } + } + if (el) { + el.style.display = ''; + } + return false; +} + +</script>''' + + +error_css = """ +<style type="text/css"> +body { + font-family: Helvetica, sans-serif; +} + +table { + width: 100%; +} + +tr.header { + background-color: #006; + color: #fff; +} + +tr.even { + background-color: #ddd; +} + +table.variables td { + vertical-align: top; + overflow: auto; +} + +a.button { + background-color: #ccc; + border: 2px outset #aaa; + color: #000; + text-decoration: none; +} + +a.button:hover { + background-color: #ddd; +} + +code.source { + color: #006; +} + +a.switch_source { + color: #090; + text-decoration: none; +} + +a.switch_source:hover { + background-color: #ddd; +} + +.source-highlight { + background-color: #ff9; +} + +</style> +""" + +def format_html(exc_data, include_hidden_frames=False, **ops): + if not include_hidden_frames: + return HTMLFormatter(**ops).format_collected_data(exc_data) + short_er = format_html(exc_data, show_hidden_frames=False, **ops) + # @@: This should have a way of seeing if the previous traceback + # was actually trimmed at all + ops['include_reusable'] = False + ops['show_extra_data'] = False + long_er = format_html(exc_data, show_hidden_frames=True, **ops) + text_er = format_text(exc_data, show_hidden_frames=True, **ops) + return """ + %s + <br> + <script type="text/javascript"> + show_button('full_traceback', 'full traceback') + </script> + <div id="full_traceback" class="hidden-data"> + %s + </div> + <br> + <script type="text/javascript"> + show_button('text_version', 'text version') + </script> + <div id="text_version" class="hidden-data"> + <textarea style="width: 100%%" rows=10 cols=60>%s</textarea> + </div> + """ % (short_er, long_er, cgi.escape(text_er)) + +def format_text(exc_data, **ops): + return TextFormatter(**ops).format_collected_data(exc_data) + +whitespace_re = re.compile(r' +') +pre_re = re.compile(r'</?pre.*?>') +error_re = re.compile(r'<h3>ERROR: .*?</h3>') + +def str2html(src, strip=False, indent_subsequent=0, + highlight_inner=False): + """ + Convert a string to HTML. Try to be really safe about it, + returning a quoted version of the string if nothing else works. + """ + try: + return _str2html(src, strip=strip, + indent_subsequent=indent_subsequent, + highlight_inner=highlight_inner) + except: + return html_quote(src) + +def _str2html(src, strip=False, indent_subsequent=0, + highlight_inner=False): + if strip: + src = src.strip() + orig_src = src + try: + src = PySourceColor.str2html(src, form='snip') + src = error_re.sub('', src) + src = pre_re.sub('', src) + src = re.sub(r'^[\n\r]{0,1}', '', src) + src = re.sub(r'[\n\r]{0,1}$', '', src) + except: + src = html_quote(orig_src) + lines = src.splitlines() + if len(lines) == 1: + return lines[0] + indent = ' '*indent_subsequent + for i in range(1, len(lines)): + lines[i] = indent+lines[i] + if highlight_inner and i == len(lines)/2: + lines[i] = '<span class="source-highlight">%s</span>' % lines[i] + src = '<br>\n'.join(lines) + src = whitespace_re.sub( + lambda m: ' '*(len(m.group(0))-1) + ' ', src) + return src + +def truncate(string, limit=1000): + """ + Truncate the string to the limit number of + characters + """ + if len(string) > limit: + return string[:limit-20]+'...'+string[-17:] + else: + return string + +def make_wrappable(html, wrap_limit=60, + split_on=';?&@!$#-/\\"\''): + # Currently using <wbr>, maybe should use ​ + # http://www.cs.tut.fi/~jkorpela/html/nobr.html + if len(html) <= wrap_limit: + return html + words = html.split() + new_words = [] + for word in words: + wrapped_word = '' + while len(word) > wrap_limit: + for char in split_on: + if char in word: + first, rest = word.split(char, 1) + wrapped_word += first+char+'<wbr>' + word = rest + break + else: + for i in range(0, len(word), wrap_limit): + wrapped_word += word[i:i+wrap_limit]+'<wbr>' + word = '' + wrapped_word += word + new_words.append(wrapped_word) + return ' '.join(new_words) + +def make_pre_wrappable(html, wrap_limit=60, + split_on=';?&@!$#-/\\"\''): + """ + Like ``make_wrappable()`` but intended for text that will + go in a ``<pre>`` block, so wrap on a line-by-line basis. + """ + lines = html.splitlines() + new_lines = [] + for line in lines: + if len(line) > wrap_limit: + for char in split_on: + if char in line: + parts = line.split(char) + line = '<wbr>'.join(parts) + break + new_lines.append(line) + return '\n'.join(lines) diff --git a/paste/exceptions/reporter.py b/paste/exceptions/reporter.py new file mode 100644 index 0000000..7c0c266 --- /dev/null +++ b/paste/exceptions/reporter.py @@ -0,0 +1,141 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import smtplib +import time +try: + from socket import sslerror +except ImportError: + sslerror = None +from paste.exceptions import formatter + +class Reporter(object): + + 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' + smtp_username = None + smtp_password = None + smtp_use_tls = False + subject_prefix = '' + + def report(self, exc_data): + msg = self.assemble_email(exc_data) + server = smtplib.SMTP(self.smtp_server) + if self.smtp_use_tls: + server.ehlo() + server.starttls() + server.ehlo() + if self.smtp_username and self.smtp_password: + server.login(self.smtp_username, self.smtp_password) + server.sendmail(self.from_address, + self.to_addresses, msg.as_string()) + try: + server.quit() + except sslerror: + # sslerror is raised in tls connections on closing sometimes + pass + + 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) + subject = '%s: %s' % (exc_data.exception_type, + formatter.truncate(str(exc_data.exception_value))) + msg['Subject'] = self.subject_prefix + subject + 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) + 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(self.exc_data)] diff --git a/paste/exceptions/serial_number_generator.py b/paste/exceptions/serial_number_generator.py new file mode 100644 index 0000000..3f80107 --- /dev/null +++ b/paste/exceptions/serial_number_generator.py @@ -0,0 +1,129 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +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) +""" + +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +import six + +good_characters = "23456789abcdefghjkmnpqrtuvwxyz" + +base = len(good_characters) + +def make_identifier(number): + """ + Encodes a number as an identifier. + """ + if not isinstance(number, six.integer_types): + 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 not callable(hasher): + # Accept sha/md5 modules as well as callables + hasher = hasher.new + 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, six.text_type): + s = s.encode('utf-8') + elif not isinstance(s, six.binary_type): + s = str(s) + if six.PY3: + s = s.encode('utf-8') + h = hasher(s) + bin_hash = h.digest() + modulo = base ** length + number = 0 + for c in list(bin_hash): + number = (number * 256 + six.byte2int([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) + 'c53' + >>> 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) + 'c53x9rqh3' + """, + 'hash_identifier': """ + >>> hash_identifier(0, 5) + 'cy2dr' + >>> hash_identifier(0, 10) + 'cy2dr6rg46' + >>> hash_identifier('this is a test of a long string', 5) + 'awatu' + >>> hash_identifier(0, 26) + 'cy2dr6rg46cx8t4w2f3nfexzk4' + >>> 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) + 'cy-2dr6-rg46' + >>> hash_identifier(0, 10, group=4, upper=True, prefix='M-') + 'M-CY-2DR6-RG46' + """} + +if __name__ == '__main__': + import doctest + doctest.testmod() + |