# $Id: exception.py 2136 2006-09-08 19:46:55Z dairiki $
# exception.py - exception classes for Myghty
# Copyright (C) 2004, 2005 Michael Bayer mike_mp@zzzcomputing.com
# Original Perl code and documentation copyright (c) 1998-2003 by Jonathan Swartz.
#
# This module is part of Myghty and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
#
import string, sys, re, traceback, os
from myghty.util import *
# get the directory of myghty, to filter it out of friendly stack traces
import myghty
MYGHTY_DIR = os.path.normcase(os.path.dirname(myghty.__file__))
class Error(Exception):
"""generic exception class"""
def __init__(self, message=None, wrapped=None, *args, **params):
self.wrapped = wrapped
if message is None:
if self.wrapped is None:
self.msg = self.__class__.__name__
else:
self.msg = "%s(%s): %s" % (
self.__class__.__name__,
self.wrapped.__class__.__name__,
self.wrapped)
else:
self.msg = self.__class__.__name__ + ": " + message
self.trace_records = []
self.lead_rec = None
self.reversefiles = {}
if self.wrapped is not None and isinstance(self.wrapped, SyntaxError):
self.lead_rec = Error.TBTraceLine(Error.SimpleTraceLine(self.wrapped.text, self.wrapped.lineno, self.wrapped.filename))
self.ispythonsyntax = True
else:
self.ispythonsyntax = False
Exception.__init__(self, *args, **params)
def initTraceback(self, interpreter = None):
""" call this right before each re-raise of the exception"""
# at the moment, initTraceback only needs to be called once, so
# return if we already got trace records
if len(self.trace_records): return
# extract stack info from the exception throw:
(type, value, trcback) = sys.exc_info()
self.raw_excinfo = (type, value, trcback)
rawrecords = traceback.extract_tb(trcback)
# add to that, stack info from the current stack frame
rawrecords = traceback.extract_stack() + rawrecords
# if a lead record was programmatically hardcoded, init that
# (it is probably a SyntaxTraceLine from Lexer)
# and dont change it
if self.lead_rec is not None:
self.lead_rec.init_reverse_file(self, interpreter)
set_lead_rec = False
else:
set_lead_rec = True
recs = []
# create TBTraceLines out of the stack frames in an attempt to sort out
# compiled templatelines, myghty library lines, and non-myghty python lines
for rec in rawrecords:
rec = Error.TBTraceLine(Error.SimpleTraceLine(rec[3], rec[1], rec[0]))
rec.init_reverse_file(self, interpreter)
if not rec.is_myghty_lib() and set_lead_rec:
self.lead_rec = rec
recs.insert(0, rec)
self.trace_records += recs
class CodeLine:
"""represents a line of code in a python source file"""
def __init__(self, linenum, line, encoding=None):
self.linenum = linenum
if line is None:
self.line = 'None'
else:
self.line = _EncodedLine(string.rstrip(line), encoding)
class TraceLine:
"""represents a trace line in a stack trace"""
def __init__(self):
self.codeline = None
self.code = None
self.file = None
self.original = None
def init_reverse_file(self, parent, interpreter):raise NotImplementedError()
def is_myghty_lib(self):
"returns True if this line corresponds to package in the myghty distribution"
return self.has_file() and os.path.commonprefix([MYGHTY_DIR, os.path.normcase(self.file)]) == MYGHTY_DIR
def get_lines(self, span = 3):raise NotImplementedError()
def get_file_lines(self, f, linenum, span):
i = 0
lines = []
encoding = self.get_source_encoding()
while True:
l = f.readline()
i += 1
if i in range(linenum - span, linenum + span + 1):
lines.append(Error.CodeLine(i, l, encoding))
if i == linenum + span: break
f.close()
return lines
def has_file(self):
return self.file is not None and os.access(self.file, os.F_OK)
def get_source_encoding(self):
raise NotImplementedError
class SyntaxTraceLine(TraceLine):
"""a traceline generated by Lexer to mark a pre-python syntax error"""
def __init__(self, code, line, name, source, file,
source_encoding=None):
self.code = code
self.line = line
self.name = name
self.file = file
self.source = source
self.init = False
self.original = self
self.codeline = self.line
if source_encoding is None:
source_encoding = sys.getdefaultencoding()
self.source_encoding = source_encoding
def init_reverse_file(self, parent, interpreter):
pass
def get_lines(self, span = 3):
f = StringIO(self.source)
return self.get_file_lines(f, self.line, span)
def get_source_encoding(self):
return self.source_encoding
class SimpleTraceLine(TraceLine):
"""a traceline for a regular python file"""
def __init__(self, code, line, file):
self.__code = code
self.line = line
self.codeline = self.line
self.original = self
self.file = file
self.compiled_record = None
def init_reverse_file(self, parent, interpreter):
pass
def _get_code(self):
if self.__code is None:
return self.get_lines(span = 0)[0].line
else:
return self.__code
code = property(_get_code)
def get_lines(self, span = 3):
if self.compiled_record is not None:
f = self.compiled_record.get_compiled_source()
elif not self.has_file():
return [Error.CodeLine(self.line, self.__code)] #FIXME: encoding
else:
f = file(self.file)
return self.get_file_lines(f, self.line, span)
def get_source_encoding(self):
if self.compiled_record is not None:
f = self.compiled_record.get_compiled_source()
elif not self.has_file():
return sys.getdefaultencoding() # FIXME: this is wrong
else:
f = file(self.file)
return _parse_encoding(f)
class TBTraceLine(TraceLine):
"""a wrapper for a simpletraceline that attempts to map its python string or file to an
originating myghty template string or file, with line numbers cross-linked between them."""
def __init__(self, original):
self.original = original
def init_reverse_file(self, parent, interpreter):
original = self.original
file = original.file
self.ismyghtylib = None
if parent.reversefiles.has_key(file):
self.reversefile = parent.reversefiles[file]
elif interpreter is not None and interpreter.reverse_lookup.has_key(file):
comprec = interpreter.reverse_lookup[file]
self.reversefile = Error.ReverseFile(interpreter, compiled_record = comprec)
parent.reversefiles[file] = self.reversefile
else:
self.reversefile = None
if self.reversefile is not None:
self.codeline = self.reversefile.get_line_number(original.codeline)
self.original.compiled_record = self.reversefile.compiled_record
if self.codeline is None:
# if we are a compiled template and the line number is within the
# area beyond user-defined code, then we are a myghtylib traceline
self.ismyghtylib = True
else:
self.codeline = None
if self.codeline is None:
self.codeline = original.codeline
self.reversefile = None
def is_myghty_lib(self):
if self.ismyghtylib is not None:
return self.ismyghtylib
else:
return Error.TraceLine.is_myghty_lib(self)
def _get_code(self):
if self.reversefile is not None:
return self.get_lines(span = 0)[0].line
else:
return self.original.code
code = property(_get_code, lambda s,p:None)
def _get_file(self):
if (
self.reversefile is not None
and
self.reversefile.compiled_record.csource.file_path is not None):
return self.reversefile.compiled_record.csource.file_path
else:
return self.original.file
file = property(_get_file, lambda s,p:None)
def get_lines(self, span = 3):
if self.reversefile is not None:
return self.get_file_lines(self.reversefile.get_source(), self.codeline, span)
else:
#return [Error.CodeLine(self.codeline, self.original.get_code())]
return self.original.get_lines(span)
def get_source_encoding(self):
return self.original.get_source_encoding()
class ReverseFile:
"""represents a python source string or file, a pointer back to
the myghty template file or string that originated it, and a cross reference of
all line numbers in the python file that map back to the template file."""
def __init__(self, interpreter, compiled_record):
self.sourcemap = []
self.compiled_record = compiled_record
self.isvalid = False
f = compiled_record.get_compiled_source()
try:
line = f.readline()
match = re.match(r"# File: (\S+) CompilerID: (.*?) Timestamp: (.*?)", line)
if not match: return
(self.path, self.compilerid, self.timestamp) = \
(match.group(1), match.group(2), match.group(3) )
self.sourcemap.append(None)
state = 0
linecounter = 0
for line in f:
if state == 0:
self.sourcemap.append(None)
match = re.match(r"\s*#\s*BEGIN BLOCK (\w+)", line)
if match:
state = 1
continue
elif state == 1:
match = re.match(r"\s*#\s*BEGIN CODE BLOCK", line)
if match:
state = 2
self.sourcemap.append(None)
continue
elif state == 2:
match = re.match(r"\s*#\s*END CODE BLOCK", line)
if match:
state = 1
self.sourcemap.append(None)
continue
if state == 1 or state == 2:
match = re.match(r"\s*#\s*SOURCE LINE (\w+)", line)
if match:
self.sourcemap.append(None)
linecounter = int(match.group(1))
else:
match = re.match(r"\s*#\s*END BLOCK (\w+)", line)
if match:
self.sourcemap.append(None)
state = 0
else:
self.sourcemap.append(linecounter)
if state == 2: linecounter += 1
self.isvalid = True
finally:
f.close()
def get_source(self):
try:
return self.compiled_record.csource.get_component_source_file()
except:
raise "Cant open file '%s'" % self.compiled_record.csource.file_path
def get_line_number(self, linenum):
if linenum < len(self.sourcemap):
return self.sourcemap[linenum - 1]
else:
return None
def htmlformat(self):
return HTMLErrorFormatter(self).format()
def textformat(self):
return PlainTextErrorFormatter(self).format()
def format(self):
return PlainTextErrorFormatter(self).format()
def singlelineformat(self):
if len(self.trace_records):
return "%s at %s line %d" % (self.msg, self.file, self.codeline)
else:
return self.msg
file =property(lambda self: self.trace_records[0].file)
codeline = property(lambda self:self.trace_records[0].codeline)
def __str__(self):
return self.singlelineformat()
def is_python_syntax(self):
return self.ispythonsyntax
def message(self):
return self.msg
class ErrorFormatter:
"""base class for an object that returns a string representation of an error"""
def format(self):
try:
return self.do_format()
except Exception, e:
return "ErrorFormatter had an exception: " + str(e)
except:
e = sys.exc_info()[0]
return "ErrorFormatter had an exception: " + str(e)
class PlainTextErrorFormatter(ErrorFormatter):
def __init__(self, error):
self.error = error
error.initTraceback(None)
self.records = self.error.trace_records
self.lead_rec = self.error.lead_rec
def do_format(self):
templatetrace = self._format_traceback(self.records, True)
realtrace = self._format_traceback(self.records, False)
if self.lead_rec is not None:
ret = (self._format_record(self.error.message(), self.lead_rec, templatetrace) +
"\n---------------------------------------------\n" +
"Original Stack Trace:\n");
if self.error.is_python_syntax():
ret += self._format_record(self.error.message(), self.lead_rec.original, realtrace)
else:
ret += self._format_record(self.error.message(), self.records[0].original, realtrace)
return ret
else:
return self._format_record(self.error.message(), self.records[0], realtrace)
def _format_traceback(self, records, friendly):
trace = ""
for rec in self.records:
if not friendly:
rec = rec.original
if not friendly or not rec.is_myghty_lib():
trace += "%s:%s\n" % (rec.file, rec.codeline)
return trace
def _format_record(self, message, rec, trace):
frec = (
message + "\n\n" +
"file: %s line %s\n" % (rec.file, rec.codeline)
)
frec += "\n" + str(rec.code);
if trace: frec += "\n\n" + trace
return frec
class HTMLErrorFormatter(ErrorFormatter):
def do_format(self):
buffer = StringIO()
try:
self.interp.execute(self.comp,
out_buffer=buffer,
output_encoding='latin1', # Default for HTML
encoding_errors='htmlentityreplace',
request_args = argdict(
records = self.records,
lead_rec = self.lead_rec,
error = self.error
))
return buffer.getvalue()
except Exception, e:
nestederror = Error(wrapped = e)
except:
nestederror = Error(wrapped = sys.exc_info()[0])
nestederror.initTraceback(self.interp)
return ("<pre>%s\n\nAdditionally, HTMLErrorFormatter had the following error:\n\n%s</pre>" %
(PlainTextErrorFormatter(self.error).format(),
PlainTextErrorFormatter(nestederror).format()))
def __init__(self, error):
import myghty.interp as interp
self.error = error
error.initTraceback(None)
self.records = self.error.trace_records
self.lead_rec = self.error.lead_rec
def handle(e, m, **params):
raise e
self.interp = interp.Interpreter(error_handler = handle)
self.comp = self.interp.make_component("""
<%args>
records
lead_rec
error
</%args>
<html>
<head>
<title>Myghty Template Error</title>
<style>
body, td {
margin: 20px;
font-family: verdana, sans-serif;
font-size: 8pt;
}
.red {
color:#FF0000;
}
.bold {
font-weight: bold;
}
</style>
</head>
<body>
% if lead_rec is not None:
<h2>Myghty Template Error</h2>
<& formatrecord, message = error.message(), lead_rec = lead_rec, records = records, friendly = True &>
<br/>
<hr/>
<h3>Original Stack Trace:</h3>
% if error.is_python_syntax():
<& formatrecord, message = error.message(), lead_rec = lead_rec.original, records = records, friendly = False &>
% else:
<& formatrecord, message = error.message(), lead_rec = records[0].original, records = records, friendly = False &>
%
% else:
<& formatrecord, message = error.message(), lead_rec = records[0], records = records, friendly = False &>
%
</body>
</html>
<%def formatrecord>
<%args>
message
lead_rec
records
friendly
</%args>
<table>
<tr><td class="bold">Error:</td>
<td><% message |h %></td>
<tr><td class="bold">File:</td>
<td><% lead_rec.file %> line <% lead_rec.codeline %></td>
</tr>
<tr>
<td class="bold" valign="top">Context:</td>
<td>
<& formatlines, lines = lead_rec.get_lines(), highlight = lead_rec.codeline &>
</td>
</tr>
<td class="bold" valign="top">Traceback:</td>
<td>
<& formattraceback, records = records, friendly = friendly &>
</td>
</tr>
</table>
</%def>
<%def formatlines>
<%args>
lines
highlight
</%args>
% for line in lines:
% code = m.apply_escapes( line.line.expandtabs(), 'h' )
% code = code.replace(' ',' ')
% if line.linenum == highlight:
<span class="red"><% line.linenum %>: <% code %></span><br/>
% else:
<% line.linenum %>: <% code %><br/>
%
</%def>
<%def formattraceback>
<%args>
records
friendly
</%args>
% for rec in records:
% if not friendly:
% rec = rec.original
% if not friendly or not rec.is_myghty_lib():
<% rec.file %>:<% rec.codeline %><br/>
%
</%def>
""", id='exception')
class Syntax(Error):
"invalid syntax in a component"
def __init__(self, error, comp_name, source_line, line_number,
source = None,
file = None,
source_encoding = None):
self.error = error
Error.__init__(self, error)
self.lead_rec = Error.SyntaxTraceLine(source_line, line_number,
comp_name,
source, file, source_encoding)
class AbortRequest(Exception):
"request aborted"
pass
class Abort(AbortRequest):
"HTTP abort called by component"
def __init__(self, aborted_value, reason):
self.aborted_value = aborted_value
self.reason = reason
class Redirected(Abort):
"redirect called"
def __init__(self, path, code=301, reason=None):
self.path = path
Abort.__init__(self, code, reason)
class Decline(AbortRequest):
"decline called by component"
def __init__(self, declined_value):
self.declined_value = declined_value
class ConfigurationError(Error):
"configuration error"
pass
class ServerError(Error):
"server error (500) "
pass
class Request(Error):
"request error"
pass
class Compiler(Error):
"compiler error"
pass
class Interpreter(Error):
"interpreter error"
pass
class MethodNotFound(Error):
"couldnt find method"
pass
class ComponentNotFound(Error):
"couldnt find component"
def __init__(self, msg, resolution_detail, silent = False):
Error.__init__(self, msg)
self.resolution_detail = resolution_detail
self.silent = silent
def message(self):
if self.resolution_detail is not None:
return self.msg + " (" + string.join(self.resolution_detail, ', ') + ")"
else:
return self.msg
def create_toplevel(self):
"""converts this ComponentNotFound into a TopLevelNotFound exception, which
is used by HTTPHandlers to return a 404 return code."""
return TopLevelNotFound(self.msg, self.resolution_detail, self.silent)
class PathIsDirectory(ComponentNotFound):
"the specified component path is a directory"
pass
class TopLevelNotFound(ComponentNotFound):
"couldnt find top level component"
pass
class MissingArgument(Error):
"a required argument was missing from a component call"
pass
class IncompatibleCompiler(Error):
"wrong compiler"
pass
################################################################
import codecs, parser
# Regexp to match python magic encoding line
_PYTHON_MAGIC_COMMENT_re = re.compile(
r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)',
re.VERBOSE)
def _parse_encoding(fp):
"""Deduce the encoding of a source file from magic comment.
It does this in the same way as the `Python interpreter`__
.. __: http://docs.python.org/ref/encodings.html
The ``fp`` argument should be a seekable file object.
"""
pos = fp.tell()
fp.seek(0)
try:
line1 = fp.readline()
has_bom = line1.startswith(codecs.BOM_UTF8)
if has_bom:
line1 = line1[len(codecs.BOM_UTF8):]
m = _PYTHON_MAGIC_COMMENT_re.match(line1)
if not m:
try:
parser.suite(line1)
except SyntaxError:
# Either it's a real syntax error, in which case the source
# is not valid python source, or line2 is a continuation of
# line1, in which case we don't want to scan line2 for a magic
# comment.
pass
else:
line2 = fp.readline()
m = _PYTHON_MAGIC_COMMENT_re.match(line2)
if has_bom:
if m:
raise SyntaxError, \
"python refuses to compile code with both a UTF8" \
" byte-order-mark and a magic encoding comment"
return 'utf_8'
elif m:
return m.group(1)
else:
return None
finally:
fp.seek(pos)
class _EncodedLine(object):
def __init__(self, line, encoding=None):
if encoding is None:
encoding = sys.getdefaultencoding()
self.line = line
self.encoding = encoding
def __str__(self):
if isinstance(self.line, str):
return self.line
return self.line.encode('ascii', 'replace')
def __unicode__(self):
if isinstance(self.line, unicode):
return self.line
return self.line.decode(self.encoding, 'replace')
def expandtabs(self, tabsize=8):
return _EncodedLine(self.line.expandtabs(tabsize), self.encoding)
|