codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import itertools4import linecache5import os6import re7import sys8import sysconfig9import traceback10import typing as t11 12from markupsafe import escape13 14from ..utils import cached_property15from .console import Console16 17HEADER = """\18<!doctype html>19<html lang=en>20 <head>21 <title>%(title)s // Werkzeug Debugger</title>22 <link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css">23 <link rel="shortcut icon"24 href="?__debugger__=yes&cmd=resource&f=console.png">25 <script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>26 <script>27 var CONSOLE_MODE = %(console)s,28 EVALEX = %(evalex)s,29 EVALEX_TRUSTED = %(evalex_trusted)s,30 SECRET = "%(secret)s";31 </script>32 </head>33 <body style="background-color: #fff">34 <div class="debugger">35"""36 37FOOTER = """\38 <div class="footer">39 Brought to you by <strong class="arthur">DON'T PANIC</strong>, your40 friendly Werkzeug powered traceback interpreter.41 </div>42 </div>43 44 <div class="pin-prompt">45 <div class="inner">46 <h3>Console Locked</h3>47 <p>48 The console is locked and needs to be unlocked by entering the PIN.49 You can find the PIN printed out on the standard output of your50 shell that runs the server.51 <form>52 <p>PIN:53 <input type=text name=pin size=14>54 <input type=submit name=btn value="Confirm Pin">55 </form>56 </div>57 </div>58 </body>59</html>60"""61 62PAGE_HTML = (63 HEADER64 + """\65<h1>%(exception_type)s</h1>66<div class="detail">67 <p class="errormsg">%(exception)s</p>68</div>69<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>70%(summary)s71<div class="plain">72 <p>73 This is the Copy/Paste friendly version of the traceback.74 </p>75 <textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>76</div>77<div class="explanation">78 The debugger caught an exception in your WSGI application. You can now79 look at the traceback which led to the error. <span class="nojavascript">80 If you enable JavaScript you can also use additional features such as code81 execution (if the evalex feature is enabled), automatic pasting of the82 exceptions and much more.</span>83</div>84"""85 + FOOTER86 + """87<!--88 89%(plaintext_cs)s90 91-->92"""93)94 95CONSOLE_HTML = (96 HEADER97 + """\98<h1>Interactive Console</h1>99<div class="explanation">100In this console you can execute Python expressions in the context of the101application. The initial namespace was created by the debugger automatically.102</div>103<div class="console"><div class="inner">The Console requires JavaScript.</div></div>104"""105 + FOOTER106)107 108SUMMARY_HTML = """\109<div class="%(classes)s">110 %(title)s111 <ul>%(frames)s</ul>112 %(description)s113</div>114"""115 116FRAME_HTML = """\117<div class="frame" id="frame-%(id)d">118 <h4>File <cite class="filename">"%(filename)s"</cite>,119 line <em class="line">%(lineno)s</em>,120 in <code class="function">%(function_name)s</code></h4>121 <div class="source %(library)s">%(lines)s</div>122</div>123"""124 125 126def _process_traceback(127 exc: BaseException,128 te: traceback.TracebackException | None = None,129 *,130 skip: int = 0,131 hide: bool = True,132) -> traceback.TracebackException:133 if te is None:134 te = traceback.TracebackException.from_exception(exc, lookup_lines=False)135 136 # Get the frames the same way StackSummary.extract did, in order137 # to match each frame with the FrameSummary to augment.138 frame_gen = traceback.walk_tb(exc.__traceback__)139 limit = getattr(sys, "tracebacklimit", None)140 141 if limit is not None:142 if limit < 0:143 limit = 0144 145 frame_gen = itertools.islice(frame_gen, limit)146 147 if skip:148 frame_gen = itertools.islice(frame_gen, skip, None)149 del te.stack[:skip]150 151 new_stack: list[DebugFrameSummary] = []152 hidden = False153 154 # Match each frame with the FrameSummary that was generated.155 # Hide frames using Paste's __traceback_hide__ rules. Replace156 # all visible FrameSummary with DebugFrameSummary.157 for (f, _), fs in zip(frame_gen, te.stack):158 if hide:159 hide_value = f.f_locals.get("__traceback_hide__", False)160 161 if hide_value in {"before", "before_and_this"}:162 new_stack = []163 hidden = False164 165 if hide_value == "before_and_this":166 continue167 elif hide_value in {"reset", "reset_and_this"}:168 hidden = False169 170 if hide_value == "reset_and_this":171 continue172 elif hide_value in {"after", "after_and_this"}:173 hidden = True174 175 if hide_value == "after_and_this":176 continue177 elif hide_value or hidden:178 continue179 180 frame_args: dict[str, t.Any] = {181 "filename": fs.filename,182 "lineno": fs.lineno,183 "name": fs.name,184 "locals": f.f_locals,185 "globals": f.f_globals,186 }187 188 if sys.version_info >= (3, 11):189 frame_args["colno"] = fs.colno190 frame_args["end_colno"] = fs.end_colno191 192 new_stack.append(DebugFrameSummary(**frame_args))193 194 # The codeop module is used to compile code from the interactive195 # debugger. Hide any codeop frames from the bottom of the traceback.196 while new_stack:197 module = new_stack[0].global_ns.get("__name__")198 199 if module is None:200 module = new_stack[0].local_ns.get("__name__")201 202 if module == "codeop":203 del new_stack[0]204 else:205 break206 207 te.stack[:] = new_stack208 209 if te.__context__:210 context_exc = t.cast(BaseException, exc.__context__)211 te.__context__ = _process_traceback(context_exc, te.__context__, hide=hide)212 213 if te.__cause__:214 cause_exc = t.cast(BaseException, exc.__cause__)215 te.__cause__ = _process_traceback(cause_exc, te.__cause__, hide=hide)216 217 return te218 219 220class DebugTraceback:221 __slots__ = ("_te", "_cache_all_tracebacks", "_cache_all_frames")222 223 def __init__(224 self,225 exc: BaseException,226 te: traceback.TracebackException | None = None,227 *,228 skip: int = 0,229 hide: bool = True,230 ) -> None:231 self._te = _process_traceback(exc, te, skip=skip, hide=hide)232 233 def __str__(self) -> str:234 return f"<{type(self).__name__} {self._te}>"235 236 @cached_property237 def all_tracebacks(238 self,239 ) -> list[tuple[str | None, traceback.TracebackException]]:240 out: list[tuple[str | None, traceback.TracebackException]] = []241 current: traceback.TracebackException | None = self._te242 243 while current is not None:244 if current.__cause__ is not None:245 chained_msg = (246 "The above exception was the direct cause of the"247 " following exception"248 )249 chained_exc = current.__cause__250 elif current.__context__ is not None and not current.__suppress_context__:251 chained_msg = (252 "During handling of the above exception, another exception occurred"253 )254 chained_exc = current.__context__255 else:256 chained_msg = None257 chained_exc = None258 259 out.append((chained_msg, current))260 current = chained_exc261 262 return out263 264 @cached_property265 def all_frames(self) -> list[DebugFrameSummary]:266 return [267 f # type: ignore[misc]268 for _, te in self.all_tracebacks269 for f in te.stack270 ]271 272 def render_traceback_text(self) -> str:273 return "".join(self._te.format())274 275 def render_traceback_html(self, include_title: bool = True) -> str:276 library_frames = [f.is_library for f in self.all_frames]277 mark_library = 0 < sum(library_frames) < len(library_frames)278 rows = []279 280 if not library_frames:281 classes = "traceback noframe-traceback"282 else:283 classes = "traceback"284 285 for msg, current in reversed(self.all_tracebacks):286 row_parts = []287 288 if msg is not None:289 row_parts.append(f'<li><div class="exc-divider">{msg}:</div>')290 291 for frame in current.stack:292 frame = t.cast(DebugFrameSummary, frame)293 info = f' title="{escape(frame.info)}"' if frame.info else ""294 row_parts.append(f"<li{info}>{frame.render_html(mark_library)}")295 296 rows.append("\n".join(row_parts))297 298 if sys.version_info < (3, 13):299 exc_type_str = self._te.exc_type.__name__300 else:301 exc_type_str = self._te.exc_type_str302 303 is_syntax_error = exc_type_str == "SyntaxError"304 305 if include_title:306 if is_syntax_error:307 title = "Syntax Error"308 else:309 title = "Traceback <em>(most recent call last)</em>:"310 else:311 title = ""312 313 exc_full = escape("".join(self._te.format_exception_only()))314 315 if is_syntax_error:316 description = f"<pre class=syntaxerror>{exc_full}</pre>"317 else:318 description = f"<blockquote>{exc_full}</blockquote>"319 320 return SUMMARY_HTML % {321 "classes": classes,322 "title": f"<h3>{title}</h3>",323 "frames": "\n".join(rows),324 "description": description,325 }326 327 def render_debugger_html(328 self, evalex: bool, secret: str, evalex_trusted: bool329 ) -> str:330 exc_lines = list(self._te.format_exception_only())331 plaintext = "".join(self._te.format())332 333 if sys.version_info < (3, 13):334 exc_type_str = self._te.exc_type.__name__335 else:336 exc_type_str = self._te.exc_type_str337 338 return PAGE_HTML % {339 "evalex": "true" if evalex else "false",340 "evalex_trusted": "true" if evalex_trusted else "false",341 "console": "false",342 "title": escape(exc_lines[0]),343 "exception": escape("".join(exc_lines)),344 "exception_type": escape(exc_type_str),345 "summary": self.render_traceback_html(include_title=False),346 "plaintext": escape(plaintext),347 "plaintext_cs": re.sub("-{2,}", "-", plaintext),348 "secret": secret,349 }350 351 352class DebugFrameSummary(traceback.FrameSummary):353 """A :class:`traceback.FrameSummary` that can evaluate code in the354 frame's namespace.355 """356 357 __slots__ = (358 "local_ns",359 "global_ns",360 "_cache_info",361 "_cache_is_library",362 "_cache_console",363 )364 365 def __init__(366 self,367 *,368 locals: dict[str, t.Any],369 globals: dict[str, t.Any],370 **kwargs: t.Any,371 ) -> None:372 super().__init__(locals=None, **kwargs)373 self.local_ns = locals374 self.global_ns = globals375 376 @cached_property377 def info(self) -> str | None:378 return self.local_ns.get("__traceback_info__")379 380 @cached_property381 def is_library(self) -> bool:382 return any(383 self.filename.startswith((path, os.path.realpath(path)))384 for path in sysconfig.get_paths().values()385 )386 387 @cached_property388 def console(self) -> Console:389 return Console(self.global_ns, self.local_ns)390 391 def eval(self, code: str) -> t.Any:392 return self.console.eval(code)393 394 def render_html(self, mark_library: bool) -> str:395 context = 5396 lines = linecache.getlines(self.filename)397 line_idx = self.lineno - 1 # type: ignore[operator]398 start_idx = max(0, line_idx - context)399 stop_idx = min(len(lines), line_idx + context + 1)400 rendered_lines = []401 402 def render_line(line: str, cls: str) -> None:403 line = line.expandtabs().rstrip()404 stripped_line = line.strip()405 prefix = len(line) - len(stripped_line)406 colno = getattr(self, "colno", 0)407 end_colno = getattr(self, "end_colno", 0)408 409 if cls == "current" and colno and end_colno:410 arrow = (411 f'\n<span class="ws">{" " * prefix}</span>'412 f"{' ' * (colno - prefix)}{'^' * (end_colno - colno)}"413 )414 else:415 arrow = ""416 417 rendered_lines.append(418 f'<pre class="line {cls}"><span class="ws">{" " * prefix}</span>'419 f"{escape(stripped_line) if stripped_line else ' '}"420 f"{arrow if arrow else ''}</pre>"421 )422 423 if line_idx < len(lines):424 for line in lines[start_idx:line_idx]:425 render_line(line, "before")426 427 render_line(lines[line_idx], "current")428 429 for line in lines[line_idx + 1 : stop_idx]:430 render_line(line, "after")431 432 return FRAME_HTML % {433 "id": id(self),434 "filename": escape(self.filename),435 "lineno": self.lineno,436 "function_name": escape(self.name),437 "lines": "\n".join(rendered_lines),438 "library": "library" if mark_library and self.is_library else "",439 }440 441 442def render_console_html(secret: str, evalex_trusted: bool) -> str:443 return CONSOLE_HTML % {444 "evalex": "true",445 "evalex_trusted": "true" if evalex_trusted else "false",446 "console": "true",447 "title": "Console",448 "secret": secret,449 }450 