You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/werkzeug/debug/tbtools.py

601 lines
19 KiB

5 years ago
import codecs
import inspect
import os
import re
import sys
import sysconfig
import traceback
import typing as t
from html import escape
5 years ago
from tokenize import TokenError
from types import CodeType
from types import TracebackType
5 years ago
from .._internal import _to_str
5 years ago
from ..filesystem import get_filesystem_encoding
from ..utils import cached_property
from .console import Console
_coding_re = re.compile(br"coding[:=]\s*([-\w.]+)")
_line_re = re.compile(br"^(.*?)$", re.MULTILINE)
_funcdef_re = re.compile(r"^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)")
HEADER = """\
5 years ago
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>%(title)s // Werkzeug Debugger</title>
<link rel="stylesheet" href="?__debugger__=yes&amp;cmd=resource&amp;f=style.css"
type="text/css">
<!-- We need to make sure this has a favicon so that the debugger does
not accidentally trigger a request to /favicon.ico which might
change the application's state. -->
5 years ago
<link rel="shortcut icon"
href="?__debugger__=yes&amp;cmd=resource&amp;f=console.png">
<script src="?__debugger__=yes&amp;cmd=resource&amp;f=debugger.js"></script>
<script type="text/javascript">
var TRACEBACK = %(traceback_id)d,
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 = """\
5 years ago
<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
+ """\
5 years ago
<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.
5 years ago
</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
+ """\
5 years ago
<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 = """\
5 years ago
<div class="%(classes)s">
%(title)s
<ul>%(frames)s</ul>
%(description)s
</div>
"""
FRAME_HTML = """\
5 years ago
<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>
"""
SOURCE_LINE_HTML = """\
5 years ago
<tr class="%(classes)s">
<td class=lineno>%(lineno)s</td>
<td>%(code)s</td>
</tr>
"""
def render_console_html(secret: str, evalex_trusted: bool = True) -> str:
5 years ago
return CONSOLE_HTML % {
"evalex": "true",
"evalex_trusted": "true" if evalex_trusted else "false",
"console": "true",
"title": "Console",
"secret": secret,
"traceback_id": -1,
}
def get_current_traceback(
ignore_system_exceptions: bool = False,
show_hidden_frames: bool = False,
skip: int = 0,
) -> "Traceback":
5 years ago
"""Get the current exception info as `Traceback` object. Per default
calling this method will reraise system exceptions such as generator exit,
system exit or others. This behavior can be disabled by passing `False`
to the function as first parameter.
"""
info = t.cast(
t.Tuple[t.Type[BaseException], BaseException, TracebackType], sys.exc_info()
)
exc_type, exc_value, tb = info
if ignore_system_exceptions and exc_type in {
SystemExit,
KeyboardInterrupt,
GeneratorExit,
}:
raise
for _ in range(skip):
5 years ago
if tb.tb_next is None:
break
tb = tb.tb_next
tb = Traceback(exc_type, exc_value, tb)
if not show_hidden_frames:
tb.filter_hidden_frames()
return tb
class Line:
5 years ago
"""Helper for the source renderer."""
__slots__ = ("lineno", "code", "in_frame", "current")
def __init__(self, lineno: int, code: str) -> None:
5 years ago
self.lineno = lineno
self.code = code
self.in_frame = False
self.current = False
@property
def classes(self) -> t.List[str]:
5 years ago
rv = ["line"]
if self.in_frame:
rv.append("in-frame")
if self.current:
rv.append("current")
return rv
def render(self) -> str:
5 years ago
return SOURCE_LINE_HTML % {
"classes": " ".join(self.classes),
5 years ago
"lineno": self.lineno,
"code": escape(self.code),
}
class Traceback:
5 years ago
"""Wraps a traceback."""
def __init__(
self,
exc_type: t.Type[BaseException],
exc_value: BaseException,
tb: TracebackType,
) -> None:
5 years ago
self.exc_type = exc_type
self.exc_value = exc_value
self.tb = tb
exception_type = exc_type.__name__
if exc_type.__module__ not in {"builtins", "__builtin__", "exceptions"}:
exception_type = f"{exc_type.__module__}.{exception_type}"
5 years ago
self.exception_type = exception_type
self.groups = []
memo = set()
while True:
self.groups.append(Group(exc_type, exc_value, tb))
memo.add(id(exc_value))
exc_value = exc_value.__cause__ or exc_value.__context__ # type: ignore
5 years ago
if exc_value is None or id(exc_value) in memo:
break
exc_type = type(exc_value)
tb = exc_value.__traceback__ # type: ignore
5 years ago
self.groups.reverse()
self.frames = [frame for group in self.groups for frame in group.frames]
def filter_hidden_frames(self) -> None:
5 years ago
"""Remove the frames according to the paste spec."""
for group in self.groups:
group.filter_hidden_frames()
self.frames[:] = [frame for group in self.groups for frame in group.frames]
@property
def is_syntax_error(self) -> bool:
5 years ago
"""Is it a syntax error?"""
return isinstance(self.exc_value, SyntaxError)
@property
def exception(self) -> str:
5 years ago
"""String representation of the final exception."""
return self.groups[-1].exception
def log(self, logfile: t.Optional[t.IO[str]] = None) -> None:
5 years ago
"""Log the ASCII traceback into a file object."""
if logfile is None:
logfile = sys.stderr
tb = f"{self.plaintext.rstrip()}\n"
logfile.write(tb)
def render_summary(self, include_title: bool = True) -> str:
5 years ago
"""Render the traceback for the interactive console."""
title = ""
classes = ["traceback"]
if not self.frames:
classes.append("noframe-traceback")
frames = []
else:
library_frames = sum(frame.is_library for frame in self.frames)
mark_lib = 0 < library_frames < len(self.frames)
frames = [group.render(mark_lib=mark_lib) for group in self.groups]
if include_title:
if self.is_syntax_error:
title = "Syntax Error"
5 years ago
else:
title = "Traceback <em>(most recent call last)</em>:"
5 years ago
if self.is_syntax_error:
description = f"<pre class=syntaxerror>{escape(self.exception)}</pre>"
5 years ago
else:
description = f"<blockquote>{escape(self.exception)}</blockquote>"
5 years ago
return SUMMARY_HTML % {
"classes": " ".join(classes),
"title": f"<h3>{title if title else ''}</h3>",
"frames": "\n".join(frames),
"description": description,
5 years ago
}
def render_full(
self,
evalex: bool = False,
secret: t.Optional[str] = None,
evalex_trusted: bool = True,
) -> str:
5 years ago
"""Render the Full HTML page with the traceback info."""
exc = escape(self.exception)
return PAGE_HTML % {
"evalex": "true" if evalex else "false",
"evalex_trusted": "true" if evalex_trusted else "false",
"console": "false",
"title": exc,
"exception": exc,
"exception_type": escape(self.exception_type),
"summary": self.render_summary(include_title=False),
"plaintext": escape(self.plaintext),
"plaintext_cs": re.sub("-{2,}", "-", self.plaintext),
"traceback_id": self.id,
"secret": secret,
}
@cached_property
def plaintext(self) -> str:
return "\n".join([group.render_text() for group in self.groups])
5 years ago
@property
def id(self) -> int:
5 years ago
return id(self)
class Group:
"""A group of frames for an exception in a traceback. If the
exception has a ``__cause__`` or ``__context__``, there are multiple
exception groups.
5 years ago
"""
def __init__(
self,
exc_type: t.Type[BaseException],
exc_value: BaseException,
tb: TracebackType,
) -> None:
5 years ago
self.exc_type = exc_type
self.exc_value = exc_value
self.info = None
if exc_value.__cause__ is not None:
self.info = (
"The above exception was the direct cause of the following exception"
)
elif exc_value.__context__ is not None:
self.info = (
"During handling of the above exception, another exception occurred"
)
5 years ago
self.frames = []
while tb is not None:
self.frames.append(Frame(exc_type, exc_value, tb))
tb = tb.tb_next # type: ignore
5 years ago
def filter_hidden_frames(self) -> None:
# An exception may not have a traceback to filter frames, such
# as one re-raised from ProcessPoolExecutor.
if not self.frames:
return
new_frames: t.List[Frame] = []
5 years ago
hidden = False
for frame in self.frames:
hide = frame.hide
if hide in ("before", "before_and_this"):
new_frames = []
hidden = False
if hide == "before_and_this":
continue
elif hide in ("reset", "reset_and_this"):
hidden = False
if hide == "reset_and_this":
continue
elif hide in ("after", "after_and_this"):
hidden = True
if hide == "after_and_this":
continue
elif hide or hidden:
continue
new_frames.append(frame)
# if we only have one frame and that frame is from the codeop
# module, remove it.
if len(new_frames) == 1 and self.frames[0].module == "codeop":
del self.frames[:]
# if the last frame is missing something went terrible wrong :(
elif self.frames[-1] in new_frames:
self.frames[:] = new_frames
@property
def exception(self) -> str:
5 years ago
"""String representation of the exception."""
buf = traceback.format_exception_only(self.exc_type, self.exc_value)
rv = "".join(buf).strip()
return _to_str(rv, "utf-8", "replace")
5 years ago
def render(self, mark_lib: bool = True) -> str:
5 years ago
out = []
if self.info is not None:
out.append(f'<li><div class="exc-divider">{self.info}:</div>')
5 years ago
for frame in self.frames:
title = f' title="{escape(frame.info)}"' if frame.info else ""
out.append(f"<li{title}>{frame.render(mark_lib=mark_lib)}")
return "\n".join(out)
5 years ago
def render_text(self) -> str:
5 years ago
out = []
if self.info is not None:
out.append(f"\n{self.info}:\n")
out.append("Traceback (most recent call last):")
5 years ago
for frame in self.frames:
out.append(frame.render_text())
out.append(self.exception)
return "\n".join(out)
5 years ago
class Frame:
5 years ago
"""A single frame in a traceback."""
def __init__(
self,
exc_type: t.Type[BaseException],
exc_value: BaseException,
tb: TracebackType,
) -> None:
5 years ago
self.lineno = tb.tb_lineno
self.function_name = tb.tb_frame.f_code.co_name
self.locals = tb.tb_frame.f_locals
self.globals = tb.tb_frame.f_globals
fn = inspect.getsourcefile(tb) or inspect.getfile(tb)
if fn[-4:] in (".pyo", ".pyc"):
fn = fn[:-1]
# if it's a file on the file system resolve the real filename.
if os.path.isfile(fn):
fn = os.path.realpath(fn)
self.filename = _to_str(fn, get_filesystem_encoding())
self.module = self.globals.get("__name__", self.locals.get("__name__"))
self.loader = self.globals.get("__loader__", self.locals.get("__loader__"))
5 years ago
self.code = tb.tb_frame.f_code
# support for paste's traceback extensions
self.hide = self.locals.get("__traceback_hide__", False)
info = self.locals.get("__traceback_info__")
if info is not None:
info = _to_str(info, "utf-8", "replace")
5 years ago
self.info = info
def render(self, mark_lib: bool = True) -> str:
5 years ago
"""Render a single frame in a traceback."""
return FRAME_HTML % {
"id": self.id,
"filename": escape(self.filename),
"lineno": self.lineno,
"function_name": escape(self.function_name),
"lines": self.render_line_context(),
"library": "library" if mark_lib and self.is_library else "",
}
@cached_property
def is_library(self) -> bool:
5 years ago
return any(
self.filename.startswith(os.path.realpath(path))
for path in sysconfig.get_paths().values()
5 years ago
)
def render_text(self) -> str:
return (
f' File "{self.filename}", line {self.lineno}, in {self.function_name}\n'
f" {self.current_line.strip()}"
5 years ago
)
def render_line_context(self) -> str:
5 years ago
before, current, after = self.get_context_lines()
rv = []
def render_line(line: str, cls: str) -> None:
5 years ago
line = line.expandtabs().rstrip()
stripped_line = line.strip()
prefix = len(line) - len(stripped_line)
rv.append(
f'<pre class="line {cls}"><span class="ws">{" " * prefix}</span>'
f"{escape(stripped_line) if stripped_line else ' '}</pre>"
5 years ago
)
for line in before:
render_line(line, "before")
render_line(current, "current")
for line in after:
render_line(line, "after")
return "\n".join(rv)
def get_annotated_lines(self) -> t.List[Line]:
5 years ago
"""Helper function that returns lines with extra information."""
lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)]
# find function definition and mark lines
if hasattr(self.code, "co_firstlineno"):
lineno = self.code.co_firstlineno - 1
while lineno > 0:
if _funcdef_re.match(lines[lineno].code):
break
lineno -= 1
try:
offset = len(inspect.getblock([f"{x.code}\n" for x in lines[lineno:]]))
5 years ago
except TokenError:
offset = 0
for line in lines[lineno : lineno + offset]:
line.in_frame = True
# mark current line
try:
lines[self.lineno - 1].current = True
except IndexError:
pass
return lines
def eval(self, code: t.Union[str, CodeType], mode: str = "single") -> t.Any:
5 years ago
"""Evaluate code in the context of the frame."""
if isinstance(code, str):
5 years ago
code = compile(code, "<interactive>", mode)
return eval(code, self.globals, self.locals)
@cached_property
def sourcelines(self) -> t.List[str]:
"""The sourcecode of the file as list of strings."""
5 years ago
# get sourcecode from loader or file
source = None
if self.loader is not None:
try:
if hasattr(self.loader, "get_source"):
source = self.loader.get_source(self.module)
elif hasattr(self.loader, "get_source_by_code"):
source = self.loader.get_source_by_code(self.code)
except Exception:
# we munch the exception so that we don't cause troubles
# if the loader is broken.
pass
if source is None:
try:
with open(self.filename, mode="rb") as f:
source = f.read()
except OSError:
5 years ago
return []
# already str? return right away
if isinstance(source, str):
5 years ago
return source.splitlines()
charset = "utf-8"
if source.startswith(codecs.BOM_UTF8):
5 years ago
source = source[3:]
else:
for idx, match in enumerate(_line_re.finditer(source)):
coding_match = _coding_re.search(match.group())
if coding_match is not None:
charset = coding_match.group(1).decode("utf-8")
5 years ago
break
if idx > 1:
break
# on broken cookies we fall back to utf-8 too
charset = _to_str(charset)
5 years ago
try:
codecs.lookup(charset)
except LookupError:
charset = "utf-8"
return source.decode(charset, "replace").splitlines()
def get_context_lines(
self, context: int = 5
) -> t.Tuple[t.List[str], str, t.List[str]]:
5 years ago
before = self.sourcelines[self.lineno - context - 1 : self.lineno - 1]
past = self.sourcelines[self.lineno : self.lineno + context]
return (before, self.current_line, past)
@property
def current_line(self) -> str:
5 years ago
try:
return self.sourcelines[self.lineno - 1]
except IndexError:
return ""
5 years ago
@cached_property
def console(self) -> Console:
5 years ago
return Console(self.globals, self.locals)
@property
def id(self) -> int:
5 years ago
return id(self)