diff options
| author | Ian Bicking <ian@ianbicking.org> | 2005-11-06 10:38:00 +0000 |
|---|---|---|
| committer | Ian Bicking <ian@ianbicking.org> | 2005-11-06 10:38:00 +0000 |
| commit | cc3e81c627ccb8ecbdb8ea0a12dadcb1b24d1027 (patch) | |
| tree | acef354087a8bca5703fc29f2687dd6d686b562c /paste | |
| parent | 93a2652ae218410ca36b40b8411138b33c7d0b83 (diff) | |
| download | paste-git-cc3e81c627ccb8ecbdb8ea0a12dadcb1b24d1027.tar.gz | |
Further refactoring and expansion of the eval exception middleware; functions now, but not that fancy yet
Diffstat (limited to 'paste')
| -rw-r--r-- | paste/evalexception/debug.js | 3 | ||||
| -rw-r--r-- | paste/evalexception/media/debug.js | 26 | ||||
| -rw-r--r-- | paste/evalexception/middleware.py | 237 | ||||
| -rw-r--r-- | paste/exceptions/formatter.py | 60 |
4 files changed, 271 insertions, 55 deletions
diff --git a/paste/evalexception/debug.js b/paste/evalexception/debug.js deleted file mode 100644 index 9729b04..0000000 --- a/paste/evalexception/debug.js +++ /dev/null @@ -1,3 +0,0 @@ -function show_frame (a, b) { - foo; -} diff --git a/paste/evalexception/media/debug.js b/paste/evalexception/media/debug.js new file mode 100644 index 0000000..f7b6c6f --- /dev/null +++ b/paste/evalexception/media/debug.js @@ -0,0 +1,26 @@ +function show_frame(anchor) { + var framecount = anchor.getAttribute('framecount'); + var expanded = anchor.expanded; + if (expanded) { + MochiKit.DOM.hideElement(anchor.expandedElement); + anchor.expanded = false; + return false; + } + anchor.expanded = true; + if (anchor.expandedElement) { + MochiKit.DOM.showElement(anchor.expandedElement); + return false; + } + var url = debug_base + + '/_debug/show_frame?framecount=' + framecount + + '&debugcount=' + debug_count; + var d = MochiKit.Async.doSimpleXMLHttpRequest(url); + d.addCallbacks(function (data) { + var el = MochiKit.DOM.DIV(); + anchor.parentNode.insertBefore(el, anchor.nextSibling); + el.innerHTML = data.responseText; + anchor.expandedElement = el; + }, function (error) { + alert('An error occurred: "' + error + '" for URL: ' + url); + }); +} diff --git a/paste/evalexception/middleware.py b/paste/evalexception/middleware.py index fa36d4e..54d43fb 100644 --- a/paste/evalexception/middleware.py +++ b/paste/evalexception/middleware.py @@ -1,30 +1,170 @@ import sys +import os import threading +import cgi +import traceback +from cStringIO import StringIO +import pprint +import itertools +import time from paste.exceptions import errormiddleware, formatter, collector +from paste import wsgilib +from paste import urlparser +from paste import httpexceptions +import cgi + +limit = 200 + +def html_quote(v): + if v is None: + return '' + return cgi.escape(str(v), 1) + +def wsgiapp(): + """ + Turns a function or method into a + """ + def decorator(func): + def application(*args): + if len(args) == 3: + environ = args[1] + start_response = args[2] + args = [args[0]] + else: + environ, start_response = args + args = [] + fs = cgi.FieldStorage( + fp=environ['wsgi.input'], + environ=environ, + keep_blank_values=1) + form = {} + for name in fs.keys(): + value = fs[name] + if not value.filename: + value = value.value + if name in form: + if isinstance(form[name], list): + form[name].append(value) + else: + form[name] = [form[name], value] + else: + form[name] = value + headers = HeaderDict({'content-type': 'text/html', + 'status': '200 OK'}) + form['environ'] = environ + form['headers'] = headers + res = func(*args, **form) + status = headers['status'] + del headers['status'] + start_response(status, headers.headeritems()) + return [res] + application.exposed = True + return application + return decorator + +def get_debug_info(func): + def replacement(self, **form): + try: + if 'debugcount' not in form: + raise ValueError('You must provide a debugcount parameter') + debugcount = form.pop('debugcount') + try: + debugcount = int(debugcount) + except ValueError: + raise ValueError('Bad value for debugcount') + if debugcount not in self.debug_infos: + raise ValueError('Debug %s no longer found (maybe it has expired?)' % debugcount) + debug_info = self.debug_infos[debugcount] + return func(self, debug_info=debug_info, **form) + except ValueError, e: + form['headers']['status'] = '500 Server Error' + return '<html>There was an error: %s</html>' % e + return replacement + + +class HeaderDict(dict): + + def __getitem__(self, key): + return dict.__getitem__(self, key.lower()) + + def __setitem__(self, key, value): + dict.__setitem__(self, key.lower(), value) + + def __delitem__(self, key): + dict.__delitem__(self, key.lower()) + + def add(self, key, value): + key = key.lower() + if key in self: + if isinstance(self[key], list): + self[key].append(value) + else: + self[key] = [self[key], value] + else: + self[key] = value + + def headeritems(self): + result = [] + for key in self: + if isinstance(self[key], list): + for v in self[key]: + result.append((key, v)) + else: + result.append((key, self[key])) + return result + +debug_counter = itertools.count(int(time.time())) class EvalException(object): def __init__(self, application, global_conf=None): self.application = application - self.debugging = False - # This is a single-threaded middleware: - self.lock = threading.Lock() - self.exc_info = None + self.debug_infos = {} def __call__(self, environ, start_response): assert not environ['wsgi.multiprocess'], ( "The EvalException middleware is not usable in a multi-process environment") - self.lock.acquire() - try: - if self.debugging: - return self.debug(environ, start_response) - else: - return self.respond(environ, start_response) - finally: - self.lock.release() + if environ.get('PATH_INFO', '').startswith('/_debug/'): + return self.debug(environ, start_response) + else: + return self.respond(environ, start_response) - def debug(self, *args): - return self.respond(*args) + def debug(self, environ, start_response): + assert wsgilib.path_info_pop(environ) == '_debug' + next_part = wsgilib.path_info_pop(environ) + method = getattr(self, next_part, None) + if not method: + return wsgilib.error_response_app( + '404 Not Found', '%r not found' % next_part)( + environ, start_response) + if not getattr(method, 'exposed', False): + return wsgilib.error_response_app( + '403 Forbidden', '%r not allowed' % next_part)( + environ, start_response) + return method(environ, start_response) + + def media(self, environ, start_response): + app = urlparser.StaticURLParser( + os.path.join(os.path.dirname(__file__), 'media')) + return app(environ, start_response) + media.exposed = True + + def mochikit(self, environ, start_response): + app = urlparser.StaticURLParser( + os.path.join(os.path.dirname(__file__), 'mochikit', 'MochiKit')) + return app(environ, start_response) + mochikit.exposed = True + + @wsgiapp() + @get_debug_info + def show_frame(self, framecount, debug_info, **kw): + frame = debug_info.frames[int(framecount)] + vars = frame.tb_frame.f_locals + if vars: + local_vars = make_table(vars) + else: + local_vars = 'No local vars' + return local_vars def respond(self, environ, start_response): base_path = environ['SCRIPT_NAME'] @@ -42,20 +182,23 @@ class EvalException(object): app_iter = self.application(environ, detect_start_response) return self.catching_iter(app_iter, environ) except: - self.exc_info = exc_info = sys.exc_info() - self.debugging = True for expected in environ.get('paste.expected_exceptions', []): if issubclass(exc_info[0], expected): raise + exc_info = sys.exc_info() + count = debug_counter.next() + debug_info = DebugInfo(count, exc_info) + assert count not in self.debug_infos + self.debug_infos[count] = debug_info if not started: start_response('500 Internal Server Error', [('content-type', 'text/html')], exc_info) # @@: it would be nice to deal with bad content types here exc_data = collector.collect_exception(*exc_info) - html = format_eval_html(exc_data, base_path) + html = format_eval_html(exc_data, base_path, count) head_html = (formatter.error_css + formatter.hide_display_js) - head_html += self.eval_javascript(base_path) + head_html += self.eval_javascript(base_path, count) page = error_template % { 'head_html': head_html, 'body': html} @@ -85,21 +228,38 @@ class EvalException(object): % close_response) yield response - def eval_javascript(self, base_path): - f = open(os.path.join(os.path.dirname(__file__), - 'evalexception.js')) - js = f.read() - f.close() - return ('<script type="text/javascript">\n' - 'debug_base = %r;\n' % base_path - + js - + '\n</script>\n') + def eval_javascript(self, base_path, counter): + return ('<script type="text/javascript" src="%s/_debug/mochikit/MochiKit.js"></script>\n' + '<script type="text/javascript" src="%s/_debug/media/debug.js"></script>\n' + '<script type="text/javascript">\n' + 'debug_base = %r;\n' + 'debug_count = %r;\n' + '\n</script>\n' + % (base_path, base_path, base_path, counter)) + +class DebugInfo(object): + + def __init__(self, counter, exc_info): + self.counter = counter + self.exc_type, self.exc_value, self.tb = exc_info + __exception_formatter__ = 1 + self.frames = [] + n = 0 + tb = self.tb + 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 + break + self.frames.append(tb) + tb = tb.tb_next + n += 1 class EvalHTMLFormatter(formatter.HTMLFormatter): - def __init__(self, base_path, **kw): + def __init__(self, base_path, counter, **kw): super(EvalHTMLFormatter, self).__init__(**kw) self.base_path = base_path + self.counter = counter self.framecount = -1 def format_source_line(self, filename, modname, lineno, name): @@ -107,15 +267,32 @@ class EvalHTMLFormatter(formatter.HTMLFormatter): self, filename, modname, lineno, name) self.framecount += 1 return (line + - ' <a href="#" frameid="%s" onClick="show_frame(this)">[+]</a>' + ' <a href="#" framecount="%s" onClick="show_frame(this)">[+]</a>' % self.framecount) -def format_eval_html(exc_data, base_path): +def make_table(items): + if isinstance(items, dict): + items = items.items() + items.sort() + rows = [] + for name, value in items: + out = StringIO() + pprint.pprint(value, out) + value = html_quote(out.getvalue()) + value = formatter.make_pre_wrappable(value) + rows.append('<tr><td>%s</td><td><pre style="overflow: auto">%s</pre><td></tr>' + % (html_quote(name), value)) + return '<table border="1">%s</table>' % ( + '\n'.join(rows)) + +def format_eval_html(exc_data, base_path, counter): short_er = EvalHTMLFormatter( base_path=base_path, + counter=counter, include_reusable=False).format_collected_data(exc_data) long_er = EvalHTMLFormatter( base_path=base_path, + counter=counter, show_hidden_frames=True, show_extra_data=False, include_reusable=False).format_collected_data(exc_data) diff --git a/paste/exceptions/formatter.py b/paste/exceptions/formatter.py index eb71ce4..d50de94 100644 --- a/paste/exceptions/formatter.py +++ b/paste/exceptions/formatter.py @@ -313,30 +313,10 @@ class HTMLFormatter(TextFormatter): % (odd and 'odd' or 'even', self.quote(name))) table.append( '<td><tt>%s</tt></td></tr>' - % self.make_wrappable(self.quote(value))) + % make_wrappable(self.quote(value))) table.append('</table>') return '\n'.join(table) - def make_wrappable(self, html, wrap_limit=60, - split_on=';?&@!$#-/\\"\''): - # Currently using <wbr>, maybe should use ​ - # http://www.cs.tut.fi/~jkorpela/html/nobr.html - words = html.split() - new_words = [] - for word in words: - if len(word) > wrap_limit: - for char in split_on: - if char in word: - words = [ - self.make_wrappable(w, wrap_limit=wrap_limit, - split_on=split_on) - for w in word.split(char, 1)] - new_words.append('<wbr>'.join(words)) - break - else: - new_words.append(word) - return ' '.join(new_words) - hide_display_js = r''' <script type="text/javascript"> function hide_display(id) { @@ -513,4 +493,40 @@ def _str2html(src, strip=False, indent_subsequent=0): lambda m: ' '*(len(m.group(0))-1) + ' ', src) return src - +def make_wrappable(html, wrap_limit=60, + split_on=';?&@!$#-/\\"\''): + # Currently using <wbr>, maybe should use ​ + # http://www.cs.tut.fi/~jkorpela/html/nobr.html + words = html.split() + new_words = [] + for word in words: + if len(word) > wrap_limit: + for char in split_on: + if char in word: + words = [ + make_wrappable(w, wrap_limit=wrap_limit, + split_on=split_on) + for w in word.split(char, 1)] + new_words.append('<wbr>'.join(words)) + break + else: + new_words.append(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) |
