tbtools.py 19 KB

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