Created
February 2, 2023 18:16
-
-
Save abersheeran/8596490a41ecc2d51b776e2a55cb6d78 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
import html | |
import inspect | |
import os | |
import traceback | |
from baize.typing import ASGIApp, Message, Receive, Scope, Send | |
from baize.asgi import Request, Response, HTMLResponse, PlainTextResponse | |
STYLES = """ | |
:root { | |
font-family: 'Segoe UI', Helvetica, Arial, sans-serif; | |
} | |
* { | |
box-sizing: border-box; | |
} | |
body { | |
margin: 0 auto; | |
padding: 0 3em; | |
min-height: calc(100vh - 3em + 1px); | |
} | |
code, pre, .code { | |
font-family: "Biaodian Pro Sans CNS", Menlo, Consolas, Courier, "Zhuyin Heiti", "Han Heiti", monospace; | |
} | |
.traceback-container { | |
border: 3px solid #038BB8; | |
} | |
.traceback-title { | |
background-color: #038BB8; | |
color: lemonchiffon; | |
padding: 15px; | |
font-size: 20px; | |
margin: 0px; | |
} | |
.frame { | |
position: relative; | |
} | |
.frame .more { | |
display: block; | |
height: 100%; | |
width: 100%; | |
opacity: 0; | |
position: absolute; | |
z-index: 1; | |
top: 0; | |
} | |
.frame .more + .detail { | |
display: none; | |
position: relative; | |
z-index: 2; | |
} | |
.frame .more:checked + .detail { | |
display: block; | |
} | |
.frame-line { | |
padding-left: 10px; | |
} | |
.center-line { | |
background-color: #038BB8; | |
color: #f9f6e1; | |
padding: 5px 0px 5px 5px; | |
} | |
.lineno { | |
margin-right: 5px; | |
} | |
.frame-title { | |
font-weight: unset; | |
padding: 15px 10px; | |
margin: 0px; | |
color: #191f21; | |
font-size: 17px; | |
border-top: 2px solid #038BB8; | |
background-color: lightgoldenrodyellow; | |
} | |
.source { | |
font-size: small; | |
background-color: lavender; | |
padding: 1em 0; | |
} | |
table { | |
width: 100%; | |
border-spacing: 0px; | |
padding: 0 10px; | |
} | |
table pre.value { | |
white-space: pre-wrap; | |
word-break: break-all; | |
} | |
table tr { | |
max-width: 100%; | |
} | |
table td { | |
padding: 10px; | |
border-top: 1px solid #c7dce8; | |
} | |
table tr td:nth-child(1) { | |
padding-left: 0px; | |
} | |
""" | |
TEMPLATE = """ | |
<html> | |
<head> | |
<style type='text/css'> | |
{styles} | |
</style> | |
<title>Index.py Debugger</title> | |
</head> | |
<body> | |
<h1>500 Server Error</h1> | |
<div class="traceback-container"> | |
<p class="traceback-title">{error}</p> | |
<div>{exc_html}</div> | |
</div> | |
<div class="environs"> | |
{environs} | |
</div> | |
</body> | |
</html> | |
""" | |
FRAME_TEMPLATE = """ | |
<div class="frame"> | |
<p class="frame-title"><code class="frame-filename">{frame_filename}</code> | |
in <b><code>{frame_name}</code></b> at line {frame_lineno}</p> | |
<input type="radio" name="more" {checked} class="more"/> | |
<div class="detail"> | |
<div id="{frame_filename}-{frame_lineno}" class="source"> | |
{code_context} | |
</div> | |
{locals} | |
</div> | |
</div> | |
""" | |
LOCAL_VARS = """ | |
<table class="local-vars"> | |
<tbody> | |
{vars} | |
</tbody> | |
</table> | |
""" | |
ENVIRON_VARS = """ | |
<table class="environ-vars"> | |
<thead> | |
<tr> | |
<td> | |
<strong>OS Environ</strong> | |
</td> | |
</tr> | |
<tr> | |
<td> | |
<strong>Name</strong> | |
</td> | |
<td> | |
<strong>Value</strong> | |
</td> | |
</tr> | |
</thead> | |
<tbody> | |
{vars} | |
</tbody> | |
</table> | |
""" | |
VAR = """ | |
<tr> | |
<td> | |
<pre class="name">{name}</pre> | |
</td> | |
<td> | |
<pre class="value">{value}</pre> | |
</td> | |
</tr> | |
""" | |
LINE = """ | |
<p><span class="frame-line code"> | |
<span class="lineno">{lineno}.</span> {line}</span></p> | |
""" | |
CENTER_LINE = """ | |
<p class="center-line"><span class="frame-line center-line code"> | |
<span class="lineno">{lineno}.</span> {line}</span></p> | |
""" | |
class DebugMiddleware: | |
def __init__(self, app: ASGIApp) -> None: | |
self.app = app | |
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | |
if scope["type"] != "http": | |
await self.app(scope, receive, send) | |
return | |
response_started = False | |
async def _send(message: Message) -> None: | |
nonlocal response_started | |
if message["type"] == "http.response.start": | |
response_started = True | |
await send(message) | |
try: | |
await self.app(scope, receive, _send) | |
except Exception as exc: | |
if not response_started: | |
request = scope["app"].factory_class.http(scope) | |
response = self.debug_response(request, exc) | |
await response(scope, receive, send) | |
raise exc | |
def format_line( | |
self, index: int, line: str, frame_lineno: int, frame_index: int | |
) -> str: | |
values = { | |
# HTML escape - line could contain < or > | |
"line": html.escape(line).replace(" ", " "), | |
"lineno": (frame_lineno - frame_index) + index, | |
} | |
if index != frame_index: | |
return LINE.format(**values) | |
return CENTER_LINE.format(**values) | |
def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str: | |
code_context = "".join( | |
self.format_line(index, line, frame.lineno, frame.index) # type: ignore | |
for index, line in enumerate(frame.code_context or []) | |
) | |
_locals_vars = frame.frame.f_locals.copy() | |
locals_var = LOCAL_VARS.format( | |
vars=( | |
"".join( | |
VAR.format(name=name, value=html.escape(format(value))) | |
for name, value in _locals_vars.items() | |
) | |
) | |
) | |
values = { | |
# HTML escape - filename could contain < or >, especially if it's a virtual file e.g. <stdin> in the REPL | |
"frame_filename": html.escape(frame.filename), | |
"frame_lineno": frame.lineno, | |
# HTML escape - if you try very hard it's possible to name a function with < or > | |
"frame_name": html.escape(frame.function), | |
"code_context": code_context, | |
"checked": "checked" if not is_collapsed else "", | |
"locals": locals_var, | |
} | |
return FRAME_TEMPLATE.format(**values) | |
def generate_html(self, exc: Exception, limit: int = 7) -> str: | |
traceback_obj = traceback.TracebackException.from_exception( | |
exc, capture_locals=True | |
) | |
exc_html = "" | |
is_collapsed = False | |
exc_traceback = exc.__traceback__ | |
if exc_traceback is not None: | |
frames = inspect.getinnerframes(exc_traceback, limit) | |
for frame in reversed(frames): | |
exc_html += self.generate_frame_html(frame, is_collapsed) | |
is_collapsed = True | |
error = f"{html.escape(traceback_obj.exc_type.__name__)}: {html.escape(str(traceback_obj))}" | |
environs = ENVIRON_VARS.format( | |
vars="".join( | |
VAR.format(name=name, value=html.escape(value)) | |
for name, value in os.environ.items() | |
) | |
) | |
return TEMPLATE.format( | |
styles=STYLES, error=error, exc_html=exc_html, environs=environs | |
) | |
def generate_plain_text(self, exc: Exception) -> str: | |
return "".join(traceback.format_tb(exc.__traceback__)) | |
def debug_response(self, request: Request, exc: Exception) -> Response: | |
if request.accepts("text/html"): | |
content = self.generate_html(exc) | |
return HTMLResponse(content, status_code=500) | |
else: | |
content = self.generate_plain_text(exc) | |
return PlainTextResponse(content, status_code=500) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment