tbtools.py 20 KB

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