tbtools.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. # -*- coding: utf-8 -*-
  2. """
  3. werkzeug.debug.tbtools
  4. ~~~~~~~~~~~~~~~~~~~~~~
  5. This module provides various traceback related utility functions.
  6. :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
  7. :license: BSD.
  8. """
  9. import re
  10. import os
  11. import sys
  12. import json
  13. import inspect
  14. import traceback
  15. import codecs
  16. from tokenize import TokenError
  17. from werkzeug.utils import cached_property, escape
  18. from werkzeug.debug.console import Console
  19. from werkzeug._compat import range_type, PY2, text_type, string_types, \
  20. to_native, to_unicode
  21. from werkzeug.filesystem import get_filesystem_encoding
  22. _coding_re = re.compile(br'coding[:=]\s*([-\w.]+)')
  23. _line_re = re.compile(br'^(.*?)$(?m)')
  24. _funcdef_re = re.compile(r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)')
  25. UTF8_COOKIE = b'\xef\xbb\xbf'
  26. system_exceptions = (SystemExit, KeyboardInterrupt)
  27. try:
  28. system_exceptions += (GeneratorExit,)
  29. except NameError:
  30. pass
  31. HEADER = u'''\
  32. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  33. "http://www.w3.org/TR/html4/loose.dtd">
  34. <html>
  35. <head>
  36. <title>%(title)s // Werkzeug Debugger</title>
  37. <link rel="stylesheet" href="?__debugger__=yes&amp;cmd=resource&amp;f=style.css"
  38. type="text/css">
  39. <!-- We need to make sure this has a favicon so that the debugger does
  40. not by accident trigger a request to /favicon.ico which might
  41. change the application state. -->
  42. <link rel="shortcut icon"
  43. href="?__debugger__=yes&amp;cmd=resource&amp;f=console.png">
  44. <script src="?__debugger__=yes&amp;cmd=resource&amp;f=jquery.js"></script>
  45. <script src="?__debugger__=yes&amp;cmd=resource&amp;f=debugger.js"></script>
  46. <script type="text/javascript">
  47. var TRACEBACK = %(traceback_id)d,
  48. CONSOLE_MODE = %(console)s,
  49. EVALEX = %(evalex)s,
  50. EVALEX_TRUSTED = %(evalex_trusted)s,
  51. SECRET = "%(secret)s";
  52. </script>
  53. </head>
  54. <body>
  55. <div class="debugger">
  56. '''
  57. FOOTER = u'''\
  58. <div class="footer">
  59. Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
  60. friendly Werkzeug powered traceback interpreter.
  61. </div>
  62. </div>
  63. <div class="pin-prompt">
  64. <div class="inner">
  65. <h3>Console Locked</h3>
  66. <p>
  67. The console is locked and needs to be unlocked by entering the PIN.
  68. You can find the PIN printed out on the standard output of your
  69. shell that runs the server.
  70. <form>
  71. <p>PIN:
  72. <input type=text name=pin size=14>
  73. <input type=submit name=btn value="Confirm Pin">
  74. </form>
  75. </div>
  76. </div>
  77. </body>
  78. </html>
  79. '''
  80. PAGE_HTML = HEADER + u'''\
  81. <h1>%(exception_type)s</h1>
  82. <div class="detail">
  83. <p class="errormsg">%(exception)s</p>
  84. </div>
  85. <h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
  86. %(summary)s
  87. <div class="plain">
  88. <form action="/?__debugger__=yes&amp;cmd=paste" method="post">
  89. <p>
  90. <input type="hidden" name="language" value="pytb">
  91. This is the Copy/Paste friendly version of the traceback. <span
  92. class="pastemessage">You can also paste this traceback into
  93. a <a href="https://gist.github.com/">gist</a>:
  94. <input type="submit" value="create paste"></span>
  95. </p>
  96. <textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>
  97. </form>
  98. </div>
  99. <div class="explanation">
  100. The debugger caught an exception in your WSGI application. You can now
  101. look at the traceback which led to the error. <span class="nojavascript">
  102. If you enable JavaScript you can also use additional features such as code
  103. execution (if the evalex feature is enabled), automatic pasting of the
  104. exceptions and much more.</span>
  105. </div>
  106. ''' + FOOTER + '''
  107. <!--
  108. %(plaintext_cs)s
  109. -->
  110. '''
  111. CONSOLE_HTML = HEADER + u'''\
  112. <h1>Interactive Console</h1>
  113. <div class="explanation">
  114. In this console you can execute Python expressions in the context of the
  115. application. The initial namespace was created by the debugger automatically.
  116. </div>
  117. <div class="console"><div class="inner">The Console requires JavaScript.</div></div>
  118. ''' + FOOTER
  119. SUMMARY_HTML = u'''\
  120. <div class="%(classes)s">
  121. %(title)s
  122. <ul>%(frames)s</ul>
  123. %(description)s
  124. </div>
  125. '''
  126. FRAME_HTML = u'''\
  127. <div class="frame" id="frame-%(id)d">
  128. <h4>File <cite class="filename">"%(filename)s"</cite>,
  129. line <em class="line">%(lineno)s</em>,
  130. in <code class="function">%(function_name)s</code></h4>
  131. <div class="source">%(lines)s</div>
  132. </div>
  133. '''
  134. SOURCE_LINE_HTML = u'''\
  135. <tr class="%(classes)s">
  136. <td class=lineno>%(lineno)s</td>
  137. <td>%(code)s</td>
  138. </tr>
  139. '''
  140. def render_console_html(secret, evalex_trusted=True):
  141. return CONSOLE_HTML % {
  142. 'evalex': 'true',
  143. 'evalex_trusted': evalex_trusted and 'true' or 'false',
  144. 'console': 'true',
  145. 'title': 'Console',
  146. 'secret': secret,
  147. 'traceback_id': -1
  148. }
  149. def get_current_traceback(ignore_system_exceptions=False,
  150. show_hidden_frames=False, skip=0):
  151. """Get the current exception info as `Traceback` object. Per default
  152. calling this method will reraise system exceptions such as generator exit,
  153. system exit or others. This behavior can be disabled by passing `False`
  154. to the function as first parameter.
  155. """
  156. exc_type, exc_value, tb = sys.exc_info()
  157. if ignore_system_exceptions and exc_type in system_exceptions:
  158. raise
  159. for x in range_type(skip):
  160. if tb.tb_next is None:
  161. break
  162. tb = tb.tb_next
  163. tb = Traceback(exc_type, exc_value, tb)
  164. if not show_hidden_frames:
  165. tb.filter_hidden_frames()
  166. return tb
  167. class Line(object):
  168. """Helper for the source renderer."""
  169. __slots__ = ('lineno', 'code', 'in_frame', 'current')
  170. def __init__(self, lineno, code):
  171. self.lineno = lineno
  172. self.code = code
  173. self.in_frame = False
  174. self.current = False
  175. def classes(self):
  176. rv = ['line']
  177. if self.in_frame:
  178. rv.append('in-frame')
  179. if self.current:
  180. rv.append('current')
  181. return rv
  182. classes = property(classes)
  183. def render(self):
  184. return SOURCE_LINE_HTML % {
  185. 'classes': u' '.join(self.classes),
  186. 'lineno': self.lineno,
  187. 'code': escape(self.code)
  188. }
  189. class Traceback(object):
  190. """Wraps a traceback."""
  191. def __init__(self, exc_type, exc_value, tb):
  192. self.exc_type = exc_type
  193. self.exc_value = exc_value
  194. if not isinstance(exc_type, str):
  195. exception_type = exc_type.__name__
  196. if exc_type.__module__ not in ('__builtin__', 'exceptions'):
  197. exception_type = exc_type.__module__ + '.' + exception_type
  198. else:
  199. exception_type = exc_type
  200. self.exception_type = exception_type
  201. # we only add frames to the list that are not hidden. This follows
  202. # the the magic variables as defined by paste.exceptions.collector
  203. self.frames = []
  204. while tb:
  205. self.frames.append(Frame(exc_type, exc_value, tb))
  206. tb = tb.tb_next
  207. def filter_hidden_frames(self):
  208. """Remove the frames according to the paste spec."""
  209. if not self.frames:
  210. return
  211. new_frames = []
  212. hidden = False
  213. for frame in self.frames:
  214. hide = frame.hide
  215. if hide in ('before', 'before_and_this'):
  216. new_frames = []
  217. hidden = False
  218. if hide == 'before_and_this':
  219. continue
  220. elif hide in ('reset', 'reset_and_this'):
  221. hidden = False
  222. if hide == 'reset_and_this':
  223. continue
  224. elif hide in ('after', 'after_and_this'):
  225. hidden = True
  226. if hide == 'after_and_this':
  227. continue
  228. elif hide or hidden:
  229. continue
  230. new_frames.append(frame)
  231. # if we only have one frame and that frame is from the codeop
  232. # module, remove it.
  233. if len(new_frames) == 1 and self.frames[0].module == 'codeop':
  234. del self.frames[:]
  235. # if the last frame is missing something went terrible wrong :(
  236. elif self.frames[-1] in new_frames:
  237. self.frames[:] = new_frames
  238. def is_syntax_error(self):
  239. """Is it a syntax error?"""
  240. return isinstance(self.exc_value, SyntaxError)
  241. is_syntax_error = property(is_syntax_error)
  242. def exception(self):
  243. """String representation of the exception."""
  244. buf = traceback.format_exception_only(self.exc_type, self.exc_value)
  245. rv = ''.join(buf).strip()
  246. return rv.decode('utf-8', 'replace') if PY2 else rv
  247. exception = property(exception)
  248. def log(self, logfile=None):
  249. """Log the ASCII traceback into a file object."""
  250. if logfile is None:
  251. logfile = sys.stderr
  252. tb = self.plaintext.rstrip() + u'\n'
  253. if PY2:
  254. tb = tb.encode('utf-8', 'replace')
  255. logfile.write(tb)
  256. def paste(self):
  257. """Create a paste and return the paste id."""
  258. data = json.dumps({
  259. 'description': 'Werkzeug Internal Server Error',
  260. 'public': False,
  261. 'files': {
  262. 'traceback.txt': {
  263. 'content': self.plaintext
  264. }
  265. }
  266. }).encode('utf-8')
  267. try:
  268. from urllib2 import urlopen
  269. except ImportError:
  270. from urllib.request import urlopen
  271. rv = urlopen('https://api.github.com/gists', data=data)
  272. resp = json.loads(rv.read().decode('utf-8'))
  273. rv.close()
  274. return {
  275. 'url': resp['html_url'],
  276. 'id': resp['id']
  277. }
  278. def render_summary(self, include_title=True):
  279. """Render the traceback for the interactive console."""
  280. title = ''
  281. frames = []
  282. classes = ['traceback']
  283. if not self.frames:
  284. classes.append('noframe-traceback')
  285. if include_title:
  286. if self.is_syntax_error:
  287. title = u'Syntax Error'
  288. else:
  289. title = u'Traceback <em>(most recent call last)</em>:'
  290. for frame in self.frames:
  291. frames.append(u'<li%s>%s' % (
  292. frame.info and u' title="%s"' % escape(frame.info) or u'',
  293. frame.render()
  294. ))
  295. if self.is_syntax_error:
  296. description_wrapper = u'<pre class=syntaxerror>%s</pre>'
  297. else:
  298. description_wrapper = u'<blockquote>%s</blockquote>'
  299. return SUMMARY_HTML % {
  300. 'classes': u' '.join(classes),
  301. 'title': title and u'<h3>%s</h3>' % title or u'',
  302. 'frames': u'\n'.join(frames),
  303. 'description': description_wrapper % escape(self.exception)
  304. }
  305. def render_full(self, evalex=False, secret=None,
  306. evalex_trusted=True):
  307. """Render the Full HTML page with the traceback info."""
  308. exc = escape(self.exception)
  309. return PAGE_HTML % {
  310. 'evalex': evalex and 'true' or 'false',
  311. 'evalex_trusted': evalex_trusted and 'true' or 'false',
  312. 'console': 'false',
  313. 'title': exc,
  314. 'exception': exc,
  315. 'exception_type': escape(self.exception_type),
  316. 'summary': self.render_summary(include_title=False),
  317. 'plaintext': escape(self.plaintext),
  318. 'plaintext_cs': re.sub('-{2,}', '-', self.plaintext),
  319. 'traceback_id': self.id,
  320. 'secret': secret
  321. }
  322. def generate_plaintext_traceback(self):
  323. """Like the plaintext attribute but returns a generator"""
  324. yield u'Traceback (most recent call last):'
  325. for frame in self.frames:
  326. yield u' File "%s", line %s, in %s' % (
  327. frame.filename,
  328. frame.lineno,
  329. frame.function_name
  330. )
  331. yield u' ' + frame.current_line.strip()
  332. yield self.exception
  333. def plaintext(self):
  334. return u'\n'.join(self.generate_plaintext_traceback())
  335. plaintext = cached_property(plaintext)
  336. id = property(lambda x: id(x))
  337. class Frame(object):
  338. """A single frame in a traceback."""
  339. def __init__(self, exc_type, exc_value, tb):
  340. self.lineno = tb.tb_lineno
  341. self.function_name = tb.tb_frame.f_code.co_name
  342. self.locals = tb.tb_frame.f_locals
  343. self.globals = tb.tb_frame.f_globals
  344. fn = inspect.getsourcefile(tb) or inspect.getfile(tb)
  345. if fn[-4:] in ('.pyo', '.pyc'):
  346. fn = fn[:-1]
  347. # if it's a file on the file system resolve the real filename.
  348. if os.path.isfile(fn):
  349. fn = os.path.realpath(fn)
  350. self.filename = to_unicode(fn, get_filesystem_encoding())
  351. self.module = self.globals.get('__name__')
  352. self.loader = self.globals.get('__loader__')
  353. self.code = tb.tb_frame.f_code
  354. # support for paste's traceback extensions
  355. self.hide = self.locals.get('__traceback_hide__', False)
  356. info = self.locals.get('__traceback_info__')
  357. if info is not None:
  358. try:
  359. info = text_type(info)
  360. except UnicodeError:
  361. info = str(info).decode('utf-8', 'replace')
  362. self.info = info
  363. def render(self):
  364. """Render a single frame in a traceback."""
  365. return FRAME_HTML % {
  366. 'id': self.id,
  367. 'filename': escape(self.filename),
  368. 'lineno': self.lineno,
  369. 'function_name': escape(self.function_name),
  370. 'lines': self.render_line_context(),
  371. }
  372. def render_line_context(self):
  373. before, current, after = self.get_context_lines()
  374. rv = []
  375. def render_line(line, cls):
  376. line = line.expandtabs().rstrip()
  377. stripped_line = line.strip()
  378. prefix = len(line) - len(stripped_line)
  379. rv.append(
  380. '<pre class="line %s"><span class="ws">%s</span>%s</pre>' % (
  381. cls, ' ' * prefix, escape(stripped_line) or ' '))
  382. for line in before:
  383. render_line(line, 'before')
  384. render_line(current, 'current')
  385. for line in after:
  386. render_line(line, 'after')
  387. return '\n'.join(rv)
  388. def get_annotated_lines(self):
  389. """Helper function that returns lines with extra information."""
  390. lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)]
  391. # find function definition and mark lines
  392. if hasattr(self.code, 'co_firstlineno'):
  393. lineno = self.code.co_firstlineno - 1
  394. while lineno > 0:
  395. if _funcdef_re.match(lines[lineno].code):
  396. break
  397. lineno -= 1
  398. try:
  399. offset = len(inspect.getblock([x.code + '\n' for x
  400. in lines[lineno:]]))
  401. except TokenError:
  402. offset = 0
  403. for line in lines[lineno:lineno + offset]:
  404. line.in_frame = True
  405. # mark current line
  406. try:
  407. lines[self.lineno - 1].current = True
  408. except IndexError:
  409. pass
  410. return lines
  411. def eval(self, code, mode='single'):
  412. """Evaluate code in the context of the frame."""
  413. if isinstance(code, string_types):
  414. if PY2 and isinstance(code, unicode): # noqa
  415. code = UTF8_COOKIE + code.encode('utf-8')
  416. code = compile(code, '<interactive>', mode)
  417. return eval(code, self.globals, self.locals)
  418. @cached_property
  419. def sourcelines(self):
  420. """The sourcecode of the file as list of unicode strings."""
  421. # get sourcecode from loader or file
  422. source = None
  423. if self.loader is not None:
  424. try:
  425. if hasattr(self.loader, 'get_source'):
  426. source = self.loader.get_source(self.module)
  427. elif hasattr(self.loader, 'get_source_by_code'):
  428. source = self.loader.get_source_by_code(self.code)
  429. except Exception:
  430. # we munch the exception so that we don't cause troubles
  431. # if the loader is broken.
  432. pass
  433. if source is None:
  434. try:
  435. f = open(to_native(self.filename, get_filesystem_encoding()),
  436. mode='rb')
  437. except IOError:
  438. return []
  439. try:
  440. source = f.read()
  441. finally:
  442. f.close()
  443. # already unicode? return right away
  444. if isinstance(source, text_type):
  445. return source.splitlines()
  446. # yes. it should be ascii, but we don't want to reject too many
  447. # characters in the debugger if something breaks
  448. charset = 'utf-8'
  449. if source.startswith(UTF8_COOKIE):
  450. source = source[3:]
  451. else:
  452. for idx, match in enumerate(_line_re.finditer(source)):
  453. match = _coding_re.search(match.group())
  454. if match is not None:
  455. charset = match.group(1)
  456. break
  457. if idx > 1:
  458. break
  459. # on broken cookies we fall back to utf-8 too
  460. charset = to_native(charset)
  461. try:
  462. codecs.lookup(charset)
  463. except LookupError:
  464. charset = 'utf-8'
  465. return source.decode(charset, 'replace').splitlines()
  466. def get_context_lines(self, context=5):
  467. before = self.sourcelines[self.lineno - context - 1:self.lineno - 1]
  468. past = self.sourcelines[self.lineno:self.lineno + context]
  469. return (
  470. before,
  471. self.current_line,
  472. past,
  473. )
  474. @property
  475. def current_line(self):
  476. try:
  477. return self.sourcelines[self.lineno - 1]
  478. except IndexError:
  479. return u''
  480. @cached_property
  481. def console(self):
  482. return Console(self.globals, self.locals)
  483. id = property(lambda x: id(x))