# -*- coding: utf-8 -*- """ werkzeug.debug.tbtools ~~~~~~~~~~~~~~~~~~~~~~ This module provides various traceback related utility functions. :copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details. :license: BSD. """ import re import os import sys import json import inspect import traceback import codecs from tokenize import TokenError from werkzeug.utils import cached_property, escape from werkzeug.debug.console import Console from werkzeug._compat import range_type, PY2, text_type, string_types _coding_re = re.compile(r'coding[:=]\s*([-\w.]+)') _line_re = re.compile(r'^(.*?)$(?m)') _funcdef_re = re.compile(r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)') UTF8_COOKIE = '\xef\xbb\xbf' system_exceptions = (SystemExit, KeyboardInterrupt) try: system_exceptions += (GeneratorExit,) except NameError: pass HEADER = u'''\ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>%(title)s // Werkzeug Debugger</title> <link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css" type="text/css"> <!-- We need to make sure this has a favicon so that the debugger does not by accident trigger a request to /favicon.ico which might change the application state. --> <link rel="shortcut icon" href="?__debugger__=yes&cmd=resource&f=console.png"> <script type="text/javascript" src="?__debugger__=yes&cmd=resource&f=jquery.js"></script> <script type="text/javascript" src="?__debugger__=yes&cmd=resource&f=debugger.js"></script> <script type="text/javascript"> var TRACEBACK = %(traceback_id)d, CONSOLE_MODE = %(console)s, EVALEX = %(evalex)s, SECRET = "%(secret)s"; </script> </head> <body> <div class="debugger"> ''' FOOTER = u'''\ <div class="footer"> Brought to you by <strong class="arthur">DON'T PANIC</strong>, your friendly Werkzeug powered traceback interpreter. </div> </div> </body> </html> ''' PAGE_HTML = HEADER + u'''\ <h1>%(exception_type)s</h1> <div class="detail"> <p class="errormsg">%(exception)s</p> </div> <h2 class="traceback">Traceback <em>(most recent call last)</em></h2> %(summary)s <div class="plain"> <form action="/?__debugger__=yes&cmd=paste" method="post"> <p> <input type="hidden" name="language" value="pytb"> This is the Copy/Paste friendly version of the traceback. <span class="pastemessage">You can also paste this traceback into a <a href="https://gist.github.com/">gist</a>: <input type="submit" value="create paste"></span> </p> <textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea> </form> </div> <div class="explanation"> The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error. <span class="nojavascript"> If you enable JavaScript you can also use additional features such as code execution (if the evalex feature is enabled), automatic pasting of the exceptions and much more.</span> </div> ''' + FOOTER + ''' <!-- %(plaintext_cs)s --> ''' CONSOLE_HTML = HEADER + u'''\ <h1>Interactive Console</h1> <div class="explanation"> In this console you can execute Python expressions in the context of the application. The initial namespace was created by the debugger automatically. </div> <div class="console"><div class="inner">The Console requires JavaScript.</div></div> ''' + FOOTER SUMMARY_HTML = u'''\ <div class="%(classes)s"> %(title)s <ul>%(frames)s</ul> %(description)s </div> ''' FRAME_HTML = u'''\ <div class="frame" id="frame-%(id)d"> <h4>File <cite class="filename">"%(filename)s"</cite>, line <em class="line">%(lineno)s</em>, in <code class="function">%(function_name)s</code></h4> <pre>%(current_line)s</pre> </div> ''' SOURCE_TABLE_HTML = u'<table class=source>%s</table>' SOURCE_LINE_HTML = u'''\ <tr class="%(classes)s"> <td class=lineno>%(lineno)s</td> <td>%(code)s</td> </tr> ''' def render_console_html(secret): return CONSOLE_HTML % { 'evalex': 'true', 'console': 'true', 'title': 'Console', 'secret': secret, 'traceback_id': -1 } def get_current_traceback(ignore_system_exceptions=False, show_hidden_frames=False, skip=0): """Get the current exception info as `Traceback` object. Per default calling this method will reraise system exceptions such as generator exit, system exit or others. This behavior can be disabled by passing `False` to the function as first parameter. """ exc_type, exc_value, tb = sys.exc_info() if ignore_system_exceptions and exc_type in system_exceptions: raise for x in range_type(skip): if tb.tb_next is None: break tb = tb.tb_next tb = Traceback(exc_type, exc_value, tb) if not show_hidden_frames: tb.filter_hidden_frames() return tb class Line(object): """Helper for the source renderer.""" __slots__ = ('lineno', 'code', 'in_frame', 'current') def __init__(self, lineno, code): self.lineno = lineno self.code = code self.in_frame = False self.current = False def classes(self): rv = ['line'] if self.in_frame: rv.append('in-frame') if self.current: rv.append('current') return rv classes = property(classes) def render(self): return SOURCE_LINE_HTML % { 'classes': u' '.join(self.classes), 'lineno': self.lineno, 'code': escape(self.code) } class Traceback(object): """Wraps a traceback.""" def __init__(self, exc_type, exc_value, tb): self.exc_type = exc_type self.exc_value = exc_value if not isinstance(exc_type, str): exception_type = exc_type.__name__ if exc_type.__module__ not in ('__builtin__', 'exceptions'): exception_type = exc_type.__module__ + '.' + exception_type else: exception_type = exc_type self.exception_type = exception_type # we only add frames to the list that are not hidden. This follows # the the magic variables as defined by paste.exceptions.collector self.frames = [] while tb: self.frames.append(Frame(exc_type, exc_value, tb)) tb = tb.tb_next def filter_hidden_frames(self): """Remove the frames according to the paste spec.""" if not self.frames: return new_frames = [] hidden = False for frame in self.frames: hide = frame.hide if hide in ('before', 'before_and_this'): new_frames = [] hidden = False if hide == 'before_and_this': continue elif hide in ('reset', 'reset_and_this'): hidden = False if hide == 'reset_and_this': continue elif hide in ('after', 'after_and_this'): hidden = True if hide == 'after_and_this': continue elif hide or hidden: continue new_frames.append(frame) # if we only have one frame and that frame is from the codeop # module, remove it. if len(new_frames) == 1 and self.frames[0].module == 'codeop': del self.frames[:] # if the last frame is missing something went terrible wrong :( elif self.frames[-1] in new_frames: self.frames[:] = new_frames def is_syntax_error(self): """Is it a syntax error?""" return isinstance(self.exc_value, SyntaxError) is_syntax_error = property(is_syntax_error) def exception(self): """String representation of the exception.""" buf = traceback.format_exception_only(self.exc_type, self.exc_value) rv = ''.join(buf).strip() return rv.decode('utf-8', 'replace') if PY2 else rv exception = property(exception) def log(self, logfile=None): """Log the ASCII traceback into a file object.""" if logfile is None: logfile = sys.stderr tb = self.plaintext.rstrip() + u'\n' if PY2: tb.encode('utf-8', 'replace') logfile.write(tb) def paste(self): """Create a paste and return the paste id.""" data = json.dumps({ 'description': 'Werkzeug Internal Server Error', 'public': False, 'files': { 'traceback.txt': { 'content': self.plaintext } } }).encode('utf-8') try: from urllib2 import urlopen except ImportError: from urllib.request import urlopen rv = urlopen('https://api.github.com/gists', data=data) resp = json.loads(rv.read().decode('utf-8')) rv.close() return { 'url': resp['html_url'], 'id': resp['id'] } def render_summary(self, include_title=True): """Render the traceback for the interactive console.""" title = '' frames = [] classes = ['traceback'] if not self.frames: classes.append('noframe-traceback') if include_title: if self.is_syntax_error: title = u'Syntax Error' else: title = u'Traceback <em>(most recent call last)</em>:' for frame in self.frames: frames.append(u'<li%s>%s' % ( frame.info and u' title="%s"' % escape(frame.info) or u'', frame.render() )) if self.is_syntax_error: description_wrapper = u'<pre class=syntaxerror>%s</pre>' else: description_wrapper = u'<blockquote>%s</blockquote>' return SUMMARY_HTML % { 'classes': u' '.join(classes), 'title': title and u'<h3>%s</h3>' % title or u'', 'frames': u'\n'.join(frames), 'description': description_wrapper % escape(self.exception) } def render_full(self, evalex=False, secret=None): """Render the Full HTML page with the traceback info.""" exc = escape(self.exception) return PAGE_HTML % { 'evalex': evalex and 'true' or 'false', 'console': 'false', 'title': exc, 'exception': exc, 'exception_type': escape(self.exception_type), 'summary': self.render_summary(include_title=False), 'plaintext': self.plaintext, 'plaintext_cs': re.sub('-{2,}', '-', self.plaintext), 'traceback_id': self.id, 'secret': secret } def generate_plaintext_traceback(self): """Like the plaintext attribute but returns a generator""" yield u'Traceback (most recent call last):' for frame in self.frames: yield u' File "%s", line %s, in %s' % ( frame.filename, frame.lineno, frame.function_name ) yield u' ' + frame.current_line.strip() yield self.exception def plaintext(self): return u'\n'.join(self.generate_plaintext_traceback()) plaintext = cached_property(plaintext) id = property(lambda x: id(x)) class Frame(object): """A single frame in a traceback.""" def __init__(self, exc_type, exc_value, tb): self.lineno = tb.tb_lineno self.function_name = tb.tb_frame.f_code.co_name self.locals = tb.tb_frame.f_locals self.globals = tb.tb_frame.f_globals fn = inspect.getsourcefile(tb) or inspect.getfile(tb) if fn[-4:] in ('.pyo', '.pyc'): fn = fn[:-1] # if it's a file on the file system resolve the real filename. if os.path.isfile(fn): fn = os.path.realpath(fn) self.filename = fn self.module = self.globals.get('__name__') self.loader = self.globals.get('__loader__') self.code = tb.tb_frame.f_code # support for paste's traceback extensions self.hide = self.locals.get('__traceback_hide__', False) info = self.locals.get('__traceback_info__') if info is not None: try: info = text_type(info) except UnicodeError: info = str(info).decode('utf-8', 'replace') self.info = info def render(self): """Render a single frame in a traceback.""" return FRAME_HTML % { 'id': self.id, 'filename': escape(self.filename), 'lineno': self.lineno, 'function_name': escape(self.function_name), 'current_line': escape(self.current_line.strip()) } def get_annotated_lines(self): """Helper function that returns lines with extra information.""" lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)] # find function definition and mark lines if hasattr(self.code, 'co_firstlineno'): lineno = self.code.co_firstlineno - 1 while lineno > 0: if _funcdef_re.match(lines[lineno].code): break lineno -= 1 try: offset = len(inspect.getblock([x.code + '\n' for x in lines[lineno:]])) except TokenError: offset = 0 for line in lines[lineno:lineno + offset]: line.in_frame = True # mark current line try: lines[self.lineno - 1].current = True except IndexError: pass return lines def render_source(self): """Render the sourcecode.""" return SOURCE_TABLE_HTML % u'\n'.join(line.render() for line in self.get_annotated_lines()) def eval(self, code, mode='single'): """Evaluate code in the context of the frame.""" if isinstance(code, string_types): if PY2 and isinstance(code, unicode): code = UTF8_COOKIE + code.encode('utf-8') code = compile(code, '<interactive>', mode) return eval(code, self.globals, self.locals) @cached_property def sourcelines(self): """The sourcecode of the file as list of unicode strings.""" # get sourcecode from loader or file source = None if self.loader is not None: try: if hasattr(self.loader, 'get_source'): source = self.loader.get_source(self.module) elif hasattr(self.loader, 'get_source_by_code'): source = self.loader.get_source_by_code(self.code) except Exception: # we munch the exception so that we don't cause troubles # if the loader is broken. pass if source is None: try: f = open(self.filename) except IOError: return [] try: source = f.read() finally: f.close() # already unicode? return right away if isinstance(source, text_type): return source.splitlines() # yes. it should be ascii, but we don't want to reject too many # characters in the debugger if something breaks charset = 'utf-8' if source.startswith(UTF8_COOKIE): source = source[3:] else: for idx, match in enumerate(_line_re.finditer(source)): match = _line_re.search(match.group()) if match is not None: charset = match.group(1) break if idx > 1: break # on broken cookies we fall back to utf-8 too try: codecs.lookup(charset) except LookupError: charset = 'utf-8' return source.decode(charset, 'replace').splitlines() @property def current_line(self): try: return self.sourcelines[self.lineno - 1] except IndexError: return u'' @cached_property def console(self): return Console(self.globals, self.locals) id = property(lambda x: id(x))