123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- # -*- 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
- from werkzeug.filesystem import get_filesystem_encoding
- _coding_re = re.compile(br'coding[:=]\s*([-\w.]+)')
- _line_re = re.compile(br'^(.*?)$(?m)')
- _funcdef_re = re.compile(r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)')
- UTF8_COOKIE = b'\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 src="?__debugger__=yes&cmd=resource&f=jquery.js"></script>
- <script 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,
- EVALEX_TRUSTED = %(evalex_trusted)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>
- <div class="pin-prompt">
- <div class="inner">
- <h3>Console Locked</h3>
- <p>
- The console is locked and needs to be unlocked by entering the PIN.
- You can find the PIN printed out on the standard output of your
- shell that runs the server.
- <form>
- <p>PIN:
- <input type=text name=pin size=14>
- <input type=submit name=btn value="Confirm Pin">
- </form>
- </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>
- <div class="source">%(lines)s</div>
- </div>
- '''
- SOURCE_LINE_HTML = u'''\
- <tr class="%(classes)s">
- <td class=lineno>%(lineno)s</td>
- <td>%(code)s</td>
- </tr>
- '''
- def render_console_html(secret, evalex_trusted=True):
- return CONSOLE_HTML % {
- 'evalex': 'true',
- 'evalex_trusted': evalex_trusted and 'true' or 'false',
- '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 <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,
- evalex_trusted=True):
- """Render the Full HTML page with the traceback info."""
- exc = escape(self.exception)
- return PAGE_HTML % {
- 'evalex': evalex and 'true' or 'false',
- 'evalex_trusted': evalex_trusted and 'true' or 'false',
- 'console': 'false',
- 'title': exc,
- 'exception': exc,
- 'exception_type': escape(self.exception_type),
- 'summary': self.render_summary(include_title=False),
- 'plaintext': escape(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, get_filesystem_encoding())
- 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),
- 'lines': self.render_line_context(),
- }
- def render_line_context(self):
- before, current, after = self.get_context_lines()
- rv = []
- def render_line(line, cls):
- line = line.expandtabs().rstrip()
- stripped_line = line.strip()
- prefix = len(line) - len(stripped_line)
- rv.append(
- '<pre class="line %s"><span class="ws">%s</span>%s</pre>' % (
- cls, ' ' * prefix, escape(stripped_line) or ' '))
- for line in before:
- render_line(line, 'before')
- render_line(current, 'current')
- for line in after:
- render_line(line, 'after')
- return '\n'.join(rv)
- 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 eval(self, code, mode='single'):
- """Evaluate code in the context of the frame."""
- if isinstance(code, string_types):
- if PY2 and isinstance(code, unicode): # noqa
- 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(to_native(self.filename, get_filesystem_encoding()),
- 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()
- def get_context_lines(self, context=5):
- before = self.sourcelines[self.lineno - context - 1:self.lineno - 1]
- past = self.sourcelines[self.lineno:self.lineno + context]
- return (
- before,
- self.current_line,
- past,
- )
- @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))
|