# -*- coding: utf-8 -*- """ werkzeug.debug.tbtools ~~~~~~~~~~~~~~~~~~~~~~ This module provides various traceback related utility functions. :copyright: (c) 2014 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, \ to_native, to_unicode _coding_re = re.compile(br'coding[:=]\s*([-\w.]+)') _line_re = re.compile(br'^(.*?)$(?m)') _funcdef_re = re.compile(r'^(\s*def\s)|(.*(? %(title)s // Werkzeug Debugger
''' FOOTER = u'''\
''' PAGE_HTML = HEADER + u'''\

%(exception_type)s

%(exception)s

Traceback (most recent call last)

%(summary)s

This is the Copy/Paste friendly version of the traceback. You can also paste this traceback into a gist:

The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error. 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.
''' + FOOTER + ''' ''' CONSOLE_HTML = HEADER + u'''\

Interactive Console

In this console you can execute Python expressions in the context of the application. The initial namespace was created by the debugger automatically.
The Console requires JavaScript.
''' + FOOTER SUMMARY_HTML = u'''\
%(title)s %(description)s
''' FRAME_HTML = u'''\

File "%(filename)s", line %(lineno)s, in %(function_name)s

%(current_line)s
''' SOURCE_TABLE_HTML = u'%s
' SOURCE_LINE_HTML = u'''\ %(lineno)s %(code)s ''' 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 = 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 (most recent call last):' for frame in self.frames: frames.append(u'%s' % ( frame.info and u' title="%s"' % escape(frame.info) or u'', frame.render() )) if self.is_syntax_error: description_wrapper = u'
%s
' else: description_wrapper = u'
%s
' return SUMMARY_HTML % { 'classes': u' '.join(classes), 'title': title and u'

%s

' % 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 = to_unicode(fn, sys.getfilesystemencoding()) 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, '', 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, mode='rb') 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 = _coding_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 charset = to_native(charset) 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))