# $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 ("
%s\n\nAdditionally, HTMLErrorFormatter had the following error:\n\n%s
" % (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 Myghty Template Error % if lead_rec is not None:

Myghty Template Error

<& formatrecord, message = error.message(), lead_rec = lead_rec, records = records, friendly = True &>

Original Stack Trace:

% 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 &> % <%def formatrecord> <%args> message lead_rec records friendly
Error: <% message |h %>
File: <% lead_rec.file %> line <% lead_rec.codeline %>
Context: <& formatlines, lines = lead_rec.get_lines(), highlight = lead_rec.codeline &>
Traceback: <& formattraceback, records = records, friendly = friendly &>
<%def formatlines> <%args> lines highlight % for line in lines: % code = m.apply_escapes( line.line.expandtabs(), 'h' ) % code = code.replace(' ',' ') % if line.linenum == highlight: <% line.linenum %>: <% code %>
% else: <% line.linenum %>: <% code %>
% <%def formattraceback> <%args> records friendly % for rec in records: % if not friendly: % rec = rec.original % if not friendly or not rec.is_myghty_lib(): <% rec.file %>:<% rec.codeline %>
% """, 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)