Made an application for supporting sustainable local businesses in San Pancho.
Never really got completed, but it has some useful Svelte components for maps that we can reuse.
http://greenspots.dctrl.space
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.
600 lines
19 KiB
600 lines
19 KiB
import codecs |
|
import inspect |
|
import os |
|
import re |
|
import sys |
|
import sysconfig |
|
import traceback |
|
import typing as t |
|
from html import escape |
|
from tokenize import TokenError |
|
from types import CodeType |
|
from types import TracebackType |
|
|
|
from .._internal import _to_str |
|
from ..filesystem import get_filesystem_encoding |
|
from ..utils import cached_property |
|
from .console import Console |
|
|
|
_coding_re = re.compile(rb"coding[:=]\s*([-\w.]+)") |
|
_line_re = re.compile(rb"^(.*?)$", re.MULTILINE) |
|
_funcdef_re = re.compile(r"^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)") |
|
|
|
HEADER = """\ |
|
<!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&cmd=resource&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. --> |
|
<link rel="shortcut icon" |
|
href="?__debugger__=yes&cmd=resource&f=console.png"> |
|
<script src="?__debugger__=yes&cmd=resource&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 = """\ |
|
<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> |
|
""" |
|
|
|
SOURCE_LINE_HTML = """\ |
|
<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: |
|
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": |
|
"""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): |
|
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: |
|
"""Helper for the source renderer.""" |
|
|
|
__slots__ = ("lineno", "code", "in_frame", "current") |
|
|
|
def __init__(self, lineno: int, code: str) -> None: |
|
self.lineno = lineno |
|
self.code = code |
|
self.in_frame = False |
|
self.current = False |
|
|
|
@property |
|
def classes(self) -> t.List[str]: |
|
rv = ["line"] |
|
if self.in_frame: |
|
rv.append("in-frame") |
|
if self.current: |
|
rv.append("current") |
|
return rv |
|
|
|
def render(self) -> str: |
|
return SOURCE_LINE_HTML % { |
|
"classes": " ".join(self.classes), |
|
"lineno": self.lineno, |
|
"code": escape(self.code), |
|
} |
|
|
|
|
|
class Traceback: |
|
"""Wraps a traceback.""" |
|
|
|
def __init__( |
|
self, |
|
exc_type: t.Type[BaseException], |
|
exc_value: BaseException, |
|
tb: TracebackType, |
|
) -> None: |
|
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}" |
|
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 |
|
if exc_value is None or id(exc_value) in memo: |
|
break |
|
exc_type = type(exc_value) |
|
tb = exc_value.__traceback__ # type: ignore |
|
self.groups.reverse() |
|
self.frames = [frame for group in self.groups for frame in group.frames] |
|
|
|
def filter_hidden_frames(self) -> None: |
|
"""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: |
|
"""Is it a syntax error?""" |
|
return isinstance(self.exc_value, SyntaxError) |
|
|
|
@property |
|
def exception(self) -> str: |
|
"""String representation of the final exception.""" |
|
return self.groups[-1].exception |
|
|
|
def log(self, logfile: t.Optional[t.IO[str]] = None) -> None: |
|
"""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: |
|
"""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" |
|
else: |
|
title = "Traceback <em>(most recent call last)</em>:" |
|
|
|
if self.is_syntax_error: |
|
description = f"<pre class=syntaxerror>{escape(self.exception)}</pre>" |
|
else: |
|
description = f"<blockquote>{escape(self.exception)}</blockquote>" |
|
|
|
return SUMMARY_HTML % { |
|
"classes": " ".join(classes), |
|
"title": f"<h3>{title if title else ''}</h3>", |
|
"frames": "\n".join(frames), |
|
"description": description, |
|
} |
|
|
|
def render_full( |
|
self, |
|
evalex: bool = False, |
|
secret: t.Optional[str] = None, |
|
evalex_trusted: bool = True, |
|
) -> str: |
|
"""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]) |
|
|
|
@property |
|
def id(self) -> int: |
|
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. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
exc_type: t.Type[BaseException], |
|
exc_value: BaseException, |
|
tb: TracebackType, |
|
) -> None: |
|
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" |
|
) |
|
|
|
self.frames = [] |
|
while tb is not None: |
|
self.frames.append(Frame(exc_type, exc_value, tb)) |
|
tb = tb.tb_next # type: ignore |
|
|
|
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] = [] |
|
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: |
|
"""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") |
|
|
|
def render(self, mark_lib: bool = True) -> str: |
|
out = [] |
|
if self.info is not None: |
|
out.append(f'<li><div class="exc-divider">{self.info}:</div>') |
|
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) |
|
|
|
def render_text(self) -> str: |
|
out = [] |
|
if self.info is not None: |
|
out.append(f"\n{self.info}:\n") |
|
out.append("Traceback (most recent call last):") |
|
for frame in self.frames: |
|
out.append(frame.render_text()) |
|
out.append(self.exception) |
|
return "\n".join(out) |
|
|
|
|
|
class Frame: |
|
"""A single frame in a traceback.""" |
|
|
|
def __init__( |
|
self, |
|
exc_type: t.Type[BaseException], |
|
exc_value: BaseException, |
|
tb: TracebackType, |
|
) -> None: |
|
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__")) |
|
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") |
|
self.info = info |
|
|
|
def render(self, mark_lib: bool = True) -> str: |
|
"""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: |
|
return any( |
|
self.filename.startswith(os.path.realpath(path)) |
|
for path in sysconfig.get_paths().values() |
|
) |
|
|
|
def render_text(self) -> str: |
|
return ( |
|
f' File "{self.filename}", line {self.lineno}, in {self.function_name}\n' |
|
f" {self.current_line.strip()}" |
|
) |
|
|
|
def render_line_context(self) -> str: |
|
before, current, after = self.get_context_lines() |
|
rv = [] |
|
|
|
def render_line(line: str, cls: str) -> None: |
|
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>" |
|
) |
|
|
|
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]: |
|
"""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:]])) |
|
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: |
|
"""Evaluate code in the context of the frame.""" |
|
if isinstance(code, str): |
|
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.""" |
|
# 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: |
|
return [] |
|
|
|
# already str? return right away |
|
if isinstance(source, str): |
|
return source.splitlines() |
|
|
|
charset = "utf-8" |
|
if source.startswith(codecs.BOM_UTF8): |
|
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") |
|
break |
|
if idx > 1: |
|
break |
|
|
|
# on broken cookies we fall back to utf-8 too |
|
charset = _to_str(charset) |
|
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]]: |
|
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: |
|
try: |
|
return self.sourcelines[self.lineno - 1] |
|
except IndexError: |
|
return "" |
|
|
|
@cached_property |
|
def console(self) -> Console: |
|
return Console(self.globals, self.locals) |
|
|
|
@property |
|
def id(self) -> int: |
|
return id(self)
|
|
|