tbtools.py 17 KB

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