123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- import itertools
- import linecache
- import os
- import re
- import sys
- import sysconfig
- import traceback
- import typing as t
- from html import escape
- from ..utils import cached_property
- from .console import Console
- HEADER = """\
- <!doctype html>
- <html lang=en>
- <head>
- <title>%(title)s // Werkzeug Debugger</title>
- <link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css">
- <link rel="shortcut icon"
- href="?__debugger__=yes&cmd=resource&f=console.png">
- <script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
- <script>
- var CONSOLE_MODE = %(console)s,
- EVALEX = %(evalex)s,
- EVALEX_TRUSTED = %(evalex_trusted)s,
- SECRET = "%(secret)s";
- </script>
- </head>
- <body style="background-color: #fff">
- <div class="debugger">
- """
- FOOTER = """\
- <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
- + """\
- <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">
- <p>
- This is the Copy/Paste friendly version of the traceback.
- </p>
- <textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>
- </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
- + """\
- <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 = """\
- <div class="%(classes)s">
- %(title)s
- <ul>%(frames)s</ul>
- %(description)s
- </div>
- """
- FRAME_HTML = """\
- <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 %(library)s">%(lines)s</div>
- </div>
- """
- def _process_traceback(
- exc: BaseException,
- te: t.Optional[traceback.TracebackException] = None,
- *,
- skip: int = 0,
- hide: bool = True,
- ) -> traceback.TracebackException:
- if te is None:
- te = traceback.TracebackException.from_exception(exc, lookup_lines=False)
- # Get the frames the same way StackSummary.extract did, in order
- # to match each frame with the FrameSummary to augment.
- frame_gen = traceback.walk_tb(exc.__traceback__)
- limit = getattr(sys, "tracebacklimit", None)
- if limit is not None:
- if limit < 0:
- limit = 0
- frame_gen = itertools.islice(frame_gen, limit)
- if skip:
- frame_gen = itertools.islice(frame_gen, skip, None)
- del te.stack[:skip]
- new_stack: t.List[DebugFrameSummary] = []
- hidden = False
- # Match each frame with the FrameSummary that was generated.
- # Hide frames using Paste's __traceback_hide__ rules. Replace
- # all visible FrameSummary with DebugFrameSummary.
- for (f, _), fs in zip(frame_gen, te.stack):
- if hide:
- hide_value = f.f_locals.get("__traceback_hide__", False)
- if hide_value in {"before", "before_and_this"}:
- new_stack = []
- hidden = False
- if hide_value == "before_and_this":
- continue
- elif hide_value in {"reset", "reset_and_this"}:
- hidden = False
- if hide_value == "reset_and_this":
- continue
- elif hide_value in {"after", "after_and_this"}:
- hidden = True
- if hide_value == "after_and_this":
- continue
- elif hide_value or hidden:
- continue
- new_stack.append(
- DebugFrameSummary(
- filename=fs.filename,
- lineno=fs.lineno,
- name=fs.name,
- locals=f.f_locals,
- globals=f.f_globals,
- )
- )
- # The codeop module is used to compile code from the interactive
- # debugger. Hide any codeop frames from the bottom of the traceback.
- while new_stack:
- module = new_stack[0].global_ns.get("__name__")
- if module is None:
- module = new_stack[0].local_ns.get("__name__")
- if module == "codeop":
- del new_stack[0]
- else:
- break
- te.stack[:] = new_stack
- if te.__context__:
- context_exc = t.cast(BaseException, exc.__context__)
- te.__context__ = _process_traceback(context_exc, te.__context__, hide=hide)
- if te.__cause__:
- cause_exc = t.cast(BaseException, exc.__cause__)
- te.__cause__ = _process_traceback(cause_exc, te.__cause__, hide=hide)
- return te
- class DebugTraceback:
- __slots__ = ("_te", "_cache_all_tracebacks", "_cache_all_frames")
- def __init__(
- self,
- exc: BaseException,
- te: t.Optional[traceback.TracebackException] = None,
- *,
- skip: int = 0,
- hide: bool = True,
- ) -> None:
- self._te = _process_traceback(exc, te, skip=skip, hide=hide)
- def __str__(self) -> str:
- return f"<{type(self).__name__} {self._te}>"
- @cached_property
- def all_tracebacks(
- self,
- ) -> t.List[t.Tuple[t.Optional[str], traceback.TracebackException]]:
- out = []
- current = self._te
- while current is not None:
- if current.__cause__ is not None:
- chained_msg = (
- "The above exception was the direct cause of the"
- " following exception"
- )
- chained_exc = current.__cause__
- elif current.__context__ is not None and not current.__suppress_context__:
- chained_msg = (
- "During handling of the above exception, another"
- " exception occurred"
- )
- chained_exc = current.__context__
- else:
- chained_msg = None
- chained_exc = None
- out.append((chained_msg, current))
- current = chained_exc
- return out
- @cached_property
- def all_frames(self) -> t.List["DebugFrameSummary"]:
- return [
- f for _, te in self.all_tracebacks for f in te.stack # type: ignore[misc]
- ]
- def render_traceback_text(self) -> str:
- return "".join(self._te.format())
- def render_traceback_html(self, include_title: bool = True) -> str:
- library_frames = [f.is_library for f in self.all_frames]
- mark_library = 0 < sum(library_frames) < len(library_frames)
- rows = []
- if not library_frames:
- classes = "traceback noframe-traceback"
- else:
- classes = "traceback"
- for msg, current in reversed(self.all_tracebacks):
- row_parts = []
- if msg is not None:
- row_parts.append(f'<li><div class="exc-divider">{msg}:</div>')
- for frame in current.stack:
- frame = t.cast(DebugFrameSummary, frame)
- info = f' title="{escape(frame.info)}"' if frame.info else ""
- row_parts.append(f"<li{info}>{frame.render_html(mark_library)}")
- rows.append("\n".join(row_parts))
- is_syntax_error = issubclass(self._te.exc_type, SyntaxError)
- if include_title:
- if is_syntax_error:
- title = "Syntax Error"
- else:
- title = "Traceback <em>(most recent call last)</em>:"
- else:
- title = ""
- exc_full = escape("".join(self._te.format_exception_only()))
- if is_syntax_error:
- description = f"<pre class=syntaxerror>{exc_full}</pre>"
- else:
- description = f"<blockquote>{exc_full}</blockquote>"
- return SUMMARY_HTML % {
- "classes": classes,
- "title": f"<h3>{title}</h3>",
- "frames": "\n".join(rows),
- "description": description,
- }
- def render_debugger_html(
- self, evalex: bool, secret: str, evalex_trusted: bool
- ) -> str:
- exc_lines = list(self._te.format_exception_only())
- plaintext = "".join(self._te.format())
- return PAGE_HTML % {
- "evalex": "true" if evalex else "false",
- "evalex_trusted": "true" if evalex_trusted else "false",
- "console": "false",
- "title": exc_lines[0],
- "exception": escape("".join(exc_lines)),
- "exception_type": escape(self._te.exc_type.__name__),
- "summary": self.render_traceback_html(include_title=False),
- "plaintext": escape(plaintext),
- "plaintext_cs": re.sub("-{2,}", "-", plaintext),
- "secret": secret,
- }
- class DebugFrameSummary(traceback.FrameSummary):
- """A :class:`traceback.FrameSummary` that can evaluate code in the
- frame's namespace.
- """
- __slots__ = (
- "local_ns",
- "global_ns",
- "_cache_info",
- "_cache_is_library",
- "_cache_console",
- )
- def __init__(
- self,
- *,
- locals: t.Dict[str, t.Any],
- globals: t.Dict[str, t.Any],
- **kwargs: t.Any,
- ) -> None:
- super().__init__(locals=None, **kwargs)
- self.local_ns = locals
- self.global_ns = globals
- @cached_property
- def info(self) -> t.Optional[str]:
- return self.local_ns.get("__traceback_info__")
- @cached_property
- def is_library(self) -> bool:
- return any(
- self.filename.startswith(os.path.realpath(path))
- for path in sysconfig.get_paths().values()
- )
- @cached_property
- def console(self) -> Console:
- return Console(self.global_ns, self.local_ns)
- def eval(self, code: str) -> t.Any:
- return self.console.eval(code)
- def render_html(self, mark_library: bool) -> str:
- context = 5
- lines = linecache.getlines(self.filename)
- line_idx = self.lineno - 1 # type: ignore[operator]
- start_idx = max(0, line_idx - context)
- stop_idx = min(len(lines), line_idx + context + 1)
- rendered_lines = []
- def render_line(line: str, cls: str) -> None:
- line = line.expandtabs().rstrip()
- stripped_line = line.strip()
- prefix = len(line) - len(stripped_line)
- rendered_lines.append(
- f'<pre class="line {cls}"><span class="ws">{" " * prefix}</span>'
- f"{escape(stripped_line) if stripped_line else ' '}</pre>"
- )
- if lines:
- for line in lines[start_idx:line_idx]:
- render_line(line, "before")
- render_line(lines[line_idx], "current")
- for line in lines[line_idx + 1 : stop_idx]:
- render_line(line, "after")
- return FRAME_HTML % {
- "id": id(self),
- "filename": escape(self.filename),
- "lineno": self.lineno,
- "function_name": escape(self.name),
- "lines": "\n".join(rendered_lines),
- "library": "library" if mark_library and self.is_library else "",
- }
- def render_console_html(secret: str, evalex_trusted: bool) -> str:
- return CONSOLE_HTML % {
- "evalex": "true",
- "evalex_trusted": "true" if evalex_trusted else "false",
- "console": "true",
- "title": "Console",
- "secret": secret,
- }
|