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