Nicer traceback formatting (#2667)
Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com> Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com> Co-authored-by: SML <smlbiobot@gmail.com>
This commit is contained in:
parent
259e458847
commit
a5d7d03413
|
@ -24,5 +24,6 @@ module = [
|
||||||
"sanic_routing.*",
|
"sanic_routing.*",
|
||||||
"aioquic.*",
|
"aioquic.*",
|
||||||
"html5tagger.*",
|
"html5tagger.*",
|
||||||
|
"tracerite.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
|
@ -875,6 +875,8 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||||
:param request: HTTP Request object
|
:param request: HTTP Request object
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.handle",
|
"http.lifecycle.handle",
|
||||||
inline=True,
|
inline=True,
|
||||||
|
|
|
@ -40,7 +40,7 @@ FULL_COLOR_LOGO = """
|
||||||
|
|
||||||
""" # noqa
|
""" # noqa
|
||||||
|
|
||||||
SVG_LOGO = """<svg id=logo alt=Sanic viewBox="0 0 964 279"><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#e1e1e1"/></svg>""" # noqa
|
SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>""" # noqa
|
||||||
|
|
||||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from traceback import extract_tb
|
||||||
from sanic.exceptions import BadRequest, SanicException
|
from sanic.exceptions import BadRequest, SanicException
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
from sanic.log import deprecation, logger
|
from sanic.log import deprecation, logger
|
||||||
|
from sanic.pages.error import ErrorPage
|
||||||
from sanic.response import html, json, text
|
from sanic.response import html, json, text
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,10 +39,9 @@ if t.TYPE_CHECKING:
|
||||||
from sanic import HTTPResponse, Request
|
from sanic import HTTPResponse, Request
|
||||||
|
|
||||||
DEFAULT_FORMAT = "auto"
|
DEFAULT_FORMAT = "auto"
|
||||||
FALLBACK_TEXT = (
|
FALLBACK_TEXT = """\
|
||||||
"The server encountered an internal error and "
|
The application encountered an unexpected error and could not continue.\
|
||||||
"cannot complete your request."
|
"""
|
||||||
)
|
|
||||||
FALLBACK_STATUS = 500
|
FALLBACK_STATUS = 500
|
||||||
JSON = "application/json"
|
JSON = "application/json"
|
||||||
|
|
||||||
|
@ -117,134 +117,18 @@ class HTMLRenderer(BaseRenderer):
|
||||||
The default fallback type.
|
The default fallback type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TRACEBACK_STYLE = """
|
|
||||||
html { font-family: sans-serif }
|
|
||||||
h2 { color: #888; }
|
|
||||||
.tb-wrapper p, dl, dd { margin: 0 }
|
|
||||||
.frame-border { margin: 1rem }
|
|
||||||
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
|
|
||||||
.frame-line, dl { margin-bottom: 0.3rem }
|
|
||||||
.frame-code, dd { font-size: 16px; padding-left: 4ch }
|
|
||||||
.tb-wrapper, dl { border: 1px solid #eee }
|
|
||||||
.tb-header,.obj-header {
|
|
||||||
background: #eee; padding: 0.3rem; font-weight: bold
|
|
||||||
}
|
|
||||||
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
|
|
||||||
"""
|
|
||||||
TRACEBACK_WRAPPER_HTML = (
|
|
||||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
|
||||||
"<div class=tb-wrapper>{frame_html}</div>"
|
|
||||||
)
|
|
||||||
TRACEBACK_BORDER = (
|
|
||||||
"<div class=frame-border>"
|
|
||||||
"The above exception was the direct cause of the following exception:"
|
|
||||||
"</div>"
|
|
||||||
)
|
|
||||||
TRACEBACK_LINE_HTML = (
|
|
||||||
"<div class=frame-line>"
|
|
||||||
"<p class=frame-descriptor>"
|
|
||||||
"File {0.filename}, line <i>{0.lineno}</i>, "
|
|
||||||
"in <code><b>{0.name}</b></code>"
|
|
||||||
"<p class=frame-code><code>{0.line}</code>"
|
|
||||||
"</div>"
|
|
||||||
)
|
|
||||||
OBJECT_WRAPPER_HTML = (
|
|
||||||
"<div class=obj-header>{title}</div>"
|
|
||||||
"<dl class={obj_type}>{display_html}</dl>"
|
|
||||||
)
|
|
||||||
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
|
|
||||||
OUTPUT_HTML = (
|
|
||||||
"<!DOCTYPE html><html lang=en>"
|
|
||||||
"<meta charset=UTF-8><title>{title}</title>\n"
|
|
||||||
"<style>{style}</style>\n"
|
|
||||||
"<h1>{title}</h1><p>{text}\n"
|
|
||||||
"{body}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def full(self) -> HTTPResponse:
|
def full(self) -> HTTPResponse:
|
||||||
return html(
|
page = ErrorPage(
|
||||||
self.OUTPUT_HTML.format(
|
debug=self.debug,
|
||||||
title=self.title,
|
title=super().title,
|
||||||
text=self.text,
|
text=super().text,
|
||||||
style=self.TRACEBACK_STYLE,
|
request=self.request,
|
||||||
body=self._generate_body(full=True),
|
exc=self.exception,
|
||||||
),
|
|
||||||
status=self.status,
|
|
||||||
)
|
)
|
||||||
|
return html(page.render(), status=self.status, headers=self.headers)
|
||||||
|
|
||||||
def minimal(self) -> HTTPResponse:
|
def minimal(self) -> HTTPResponse:
|
||||||
return html(
|
return self.full()
|
||||||
self.OUTPUT_HTML.format(
|
|
||||||
title=self.title,
|
|
||||||
text=self.text,
|
|
||||||
style=self.TRACEBACK_STYLE,
|
|
||||||
body=self._generate_body(full=False),
|
|
||||||
),
|
|
||||||
status=self.status,
|
|
||||||
headers=self.headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
return escape(super().text)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self):
|
|
||||||
return escape(f"⚠️ {super().title}")
|
|
||||||
|
|
||||||
def _generate_body(self, *, full):
|
|
||||||
lines = []
|
|
||||||
if full:
|
|
||||||
_, exc_value, __ = sys.exc_info()
|
|
||||||
exceptions = []
|
|
||||||
while exc_value:
|
|
||||||
exceptions.append(self._format_exc(exc_value))
|
|
||||||
exc_value = exc_value.__cause__
|
|
||||||
|
|
||||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
|
||||||
appname = escape(self.request.app.name)
|
|
||||||
name = escape(self.exception.__class__.__name__)
|
|
||||||
value = escape(self.exception)
|
|
||||||
path = escape(self.request.path)
|
|
||||||
lines += [
|
|
||||||
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
|
|
||||||
f"{traceback_html}",
|
|
||||||
"<div class=summary><p>",
|
|
||||||
f"<b>{name}: {value}</b> "
|
|
||||||
f"while handling path <code>{path}</code>",
|
|
||||||
"</div>",
|
|
||||||
]
|
|
||||||
|
|
||||||
for attr, display in (("context", True), ("extra", bool(full))):
|
|
||||||
info = getattr(self.exception, attr, None)
|
|
||||||
if info and display:
|
|
||||||
lines.append(self._generate_object_display(info, attr))
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def _generate_object_display(
|
|
||||||
self, obj: t.Dict[str, t.Any], descriptor: str
|
|
||||||
) -> str:
|
|
||||||
display = "".join(
|
|
||||||
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
|
|
||||||
for key, value in obj.items()
|
|
||||||
)
|
|
||||||
return self.OBJECT_WRAPPER_HTML.format(
|
|
||||||
title=descriptor.title(),
|
|
||||||
display_html=display,
|
|
||||||
obj_type=descriptor.lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _format_exc(self, exc):
|
|
||||||
frames = extract_tb(exc.__traceback__)
|
|
||||||
frame_html = "".join(
|
|
||||||
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
|
|
||||||
)
|
|
||||||
return self.TRACEBACK_WRAPPER_HTML.format(
|
|
||||||
exc_name=escape(exc.__class__.__name__),
|
|
||||||
exc_value=escape(exc),
|
|
||||||
frame_html=frame_html,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TextRenderer(BaseRenderer):
|
class TextRenderer(BaseRenderer):
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from html5tagger import HTML, Document
|
from html5tagger import HTML, Builder, Document
|
||||||
|
|
||||||
from sanic import __version__ as VERSION
|
from sanic import __version__ as VERSION
|
||||||
from sanic.application.logo import SVG_LOGO
|
from sanic.application.logo import SVG_LOGO_SIMPLE
|
||||||
from sanic.pages.css import CSS
|
from sanic.pages.css import CSS
|
||||||
|
|
||||||
|
|
||||||
class BasePage(ABC, metaclass=CSS): # no cov
|
class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
TITLE = "Unknown"
|
TITLE = "Sanic"
|
||||||
|
HEADING = None
|
||||||
CSS: str
|
CSS: str
|
||||||
|
doc: Builder
|
||||||
|
|
||||||
def __init__(self, debug: bool = True) -> None:
|
def __init__(self, debug: bool = True) -> None:
|
||||||
self.doc = Document(self.TITLE, lang="en")
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -20,6 +21,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
return self.CSS
|
return self.CSS
|
||||||
|
|
||||||
def render(self) -> str:
|
def render(self) -> str:
|
||||||
|
self.doc = Document(self.TITLE, lang="en", id="sanic")
|
||||||
self._head()
|
self._head()
|
||||||
self._body()
|
self._body()
|
||||||
self._foot()
|
self._foot()
|
||||||
|
@ -28,7 +30,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
def _head(self) -> None:
|
def _head(self) -> None:
|
||||||
self.doc.style(HTML(self.style))
|
self.doc.style(HTML(self.style))
|
||||||
with self.doc.header:
|
with self.doc.header:
|
||||||
self.doc.div(self.TITLE)
|
self.doc.div(self.HEADING or self.TITLE)
|
||||||
|
|
||||||
def _foot(self) -> None:
|
def _foot(self) -> None:
|
||||||
with self.doc.footer:
|
with self.doc.footer:
|
||||||
|
@ -37,6 +39,23 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
self._sanic_logo()
|
self._sanic_logo()
|
||||||
if self.debug:
|
if self.debug:
|
||||||
self.doc.div(f"Version {VERSION}")
|
self.doc.div(f"Version {VERSION}")
|
||||||
|
with self.doc.div:
|
||||||
|
for idx, (title, href) in enumerate(
|
||||||
|
(
|
||||||
|
("Docs", "https://sanic.dev"),
|
||||||
|
("Help", "https://sanic.dev/en/help.html"),
|
||||||
|
("GitHub", "https://github.com/sanic-org/sanic"),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
if idx > 0:
|
||||||
|
self.doc(" | ")
|
||||||
|
self.doc.a(
|
||||||
|
title,
|
||||||
|
href=href,
|
||||||
|
target="_blank",
|
||||||
|
referrerpolicy="no-referrer",
|
||||||
|
)
|
||||||
|
self.doc.div("DEBUG mode")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _body(self) -> None:
|
def _body(self) -> None:
|
||||||
|
@ -44,7 +63,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
|
|
||||||
def _sanic_logo(self) -> None:
|
def _sanic_logo(self) -> None:
|
||||||
self.doc.a(
|
self.doc.a(
|
||||||
HTML(SVG_LOGO),
|
HTML(SVG_LOGO_SIMPLE),
|
||||||
href="https://sanic.dev",
|
href="https://sanic.dev",
|
||||||
target="_blank",
|
target="_blank",
|
||||||
referrerpolicy="no-referrer",
|
referrerpolicy="no-referrer",
|
||||||
|
|
|
@ -24,8 +24,8 @@ class CSS(ABCMeta):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
Page = super().__new__(cls, name, bases, attrs)
|
Page = super().__new__(cls, name, bases, attrs)
|
||||||
# Use a locally defined STYLE or the one from styles directory
|
# Use a locally defined STYLE or the one from styles directory
|
||||||
s = _extract_style(attrs.get("STYLE"), name)
|
Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name)
|
||||||
Page.STYLE = f"\n/* {name} */\n{s.strip()}\n" if s else ""
|
Page.STYLE += attrs.get("STYLE_APPEND", "")
|
||||||
# Combine with all ancestor styles
|
# Combine with all ancestor styles
|
||||||
Page.CSS = "".join(
|
Page.CSS = "".join(
|
||||||
Class.STYLE
|
Class.STYLE
|
||||||
|
|
109
sanic/pages/error.py
Normal file
109
sanic/pages/error.py
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import tracerite.html
|
||||||
|
|
||||||
|
from html5tagger import E
|
||||||
|
from tracerite import html_traceback, inspector
|
||||||
|
|
||||||
|
from sanic.request import Request
|
||||||
|
|
||||||
|
from .base import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
# Avoid showing the request in the traceback variable inspectors
|
||||||
|
inspector.blacklist_types += (Request,)
|
||||||
|
|
||||||
|
ENDUSER_TEXT = """\
|
||||||
|
We're sorry, but it looks like something went wrong. Please try refreshing \
|
||||||
|
the page or navigating back to the homepage. If the issue persists, our \
|
||||||
|
technical team is working to resolve it as soon as possible. We apologize \
|
||||||
|
for the inconvenience and appreciate your patience.\
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorPage(BasePage):
|
||||||
|
STYLE_APPEND = tracerite.html.style
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
debug: bool,
|
||||||
|
title: str,
|
||||||
|
text: str,
|
||||||
|
request: Request,
|
||||||
|
exc: Exception,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(debug)
|
||||||
|
name = request.app.name.replace("_", " ").strip()
|
||||||
|
if name.islower():
|
||||||
|
name = name.title()
|
||||||
|
self.TITLE = f"Application {name} cannot handle your request"
|
||||||
|
self.HEADING = E("Application ").strong(name)(
|
||||||
|
" cannot handle your request"
|
||||||
|
)
|
||||||
|
self.title = title
|
||||||
|
self.text = text
|
||||||
|
self.request = request
|
||||||
|
self.exc = exc
|
||||||
|
self.details_open = not getattr(exc, "quiet", False)
|
||||||
|
|
||||||
|
def _head(self) -> None:
|
||||||
|
self.doc._script(tracerite.html.javascript)
|
||||||
|
super()._head()
|
||||||
|
|
||||||
|
def _body(self) -> None:
|
||||||
|
debug = self.request.app.debug
|
||||||
|
route_name = self.request.name or "[route not found]"
|
||||||
|
with self.doc.main:
|
||||||
|
self.doc.h1(f"⚠️ {self.title}").p(self.text)
|
||||||
|
# Show context details if available on the exception
|
||||||
|
context = getattr(self.exc, "context", None)
|
||||||
|
if context:
|
||||||
|
self._key_value_table(
|
||||||
|
"Issue context", "exception-context", context
|
||||||
|
)
|
||||||
|
|
||||||
|
if not debug:
|
||||||
|
with self.doc.div(id="enduser"):
|
||||||
|
self.doc.p(ENDUSER_TEXT).p.a("Front Page", href="/")
|
||||||
|
return
|
||||||
|
# Show additional details in debug mode,
|
||||||
|
# open by default for 500 errors
|
||||||
|
with self.doc.details(open=self.details_open, class_="smalltext"):
|
||||||
|
# Show extra details if available on the exception
|
||||||
|
extra = getattr(self.exc, "extra", None)
|
||||||
|
if extra:
|
||||||
|
self._key_value_table(
|
||||||
|
"Issue extra data", "exception-extra", extra
|
||||||
|
)
|
||||||
|
|
||||||
|
self.doc.summary(
|
||||||
|
"Details for developers (Sanic debug mode only)"
|
||||||
|
)
|
||||||
|
if self.exc:
|
||||||
|
with self.doc.div(class_="exception-wrapper"):
|
||||||
|
self.doc.h2(f"Exception in {route_name}:")
|
||||||
|
self.doc(
|
||||||
|
html_traceback(self.exc, include_js_css=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._key_value_table(
|
||||||
|
f"{self.request.method} {self.request.path}",
|
||||||
|
"request-headers",
|
||||||
|
self.request.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _key_value_table(
|
||||||
|
self, title: str, table_id: str, data: Mapping[str, Any]
|
||||||
|
) -> None:
|
||||||
|
with self.doc.div(class_="key-value-display"):
|
||||||
|
self.doc.h2(title)
|
||||||
|
with self.doc.dl(id=table_id, class_="key-value-table smalltext"):
|
||||||
|
for key, value in data.items():
|
||||||
|
# Reading values may cause a new exception, so suppress it
|
||||||
|
try:
|
||||||
|
value = str(value)
|
||||||
|
except Exception:
|
||||||
|
value = E.em("Unable to display value")
|
||||||
|
self.doc.dt.span(key, class_="nobr key").span(": ").dd(
|
||||||
|
value
|
||||||
|
)
|
|
@ -1,37 +1,93 @@
|
||||||
|
/** BasePage **/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sanic: #ff0d68;
|
||||||
|
--sanic-yellow: #FFE900;
|
||||||
|
--sanic-background: #efeced;
|
||||||
|
--sanic-text: #121010;
|
||||||
|
--sanic-text-lighter: #756169;
|
||||||
|
--sanic-link: #ff0d68;
|
||||||
|
--sanic-block-background: #f7f4f6;
|
||||||
|
--sanic-block-text: #000;
|
||||||
|
--sanic-block-alt-text: #6b6468;
|
||||||
|
--sanic-header-background: #272325;
|
||||||
|
--sanic-header-border: #fff;
|
||||||
|
--sanic-header-text: #fff;
|
||||||
|
--sanic-highlight-background: var(--sanic-yellow);
|
||||||
|
--sanic-highlight-text: var(--sanic-text);
|
||||||
|
--sanic-tab-background: #f7f4f6;
|
||||||
|
--sanic-tab-shadow: #f7f6f6;
|
||||||
|
--sanic-tab-text: #222021;
|
||||||
|
--sanic-tracerite-var: var(--sanic-text);
|
||||||
|
--sanic-tracerite-val: #ff0d68;
|
||||||
|
--sanic-tracerite-type: #6d6a6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--sanic-text: #f7f4f6;
|
||||||
|
--sanic-background: #121010;
|
||||||
|
--sanic-block-background: #0f0d0e;
|
||||||
|
--sanic-block-text: #f7f4f6;
|
||||||
|
--sanic-header-background: #030203;
|
||||||
|
--sanic-header-border: #000;
|
||||||
|
--sanic-highlight-text: var(--sanic-background);
|
||||||
|
--sanic-tab-background: #292728;
|
||||||
|
--sanic-tab-shadow: #0f0d0e;
|
||||||
|
--sanic-tab-text: #aea7ab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font: 16px sans-serif;
|
font: 16px sans-serif;
|
||||||
background: #eee;
|
background: var(--sanic-background);
|
||||||
color: #111;
|
color: var(--sanic-text);
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
line-height: 125%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body>* {
|
body>* {
|
||||||
padding: 1rem 2vw;
|
padding: 1rem 2vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1000px) {
|
||||||
body>* {
|
body>* {
|
||||||
padding: 0.5rem 1.5vw;
|
padding: 0.5rem 1.5vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
html {
|
||||||
font-size: 1rem;
|
/* Scale everything by rem of 6px-16px by viewport width */
|
||||||
|
font-size: calc(6px + 10 * 100vw / 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
/* Make sure the footer is closer to bottom */
|
||||||
|
min-height: 70vh;
|
||||||
|
/* Generous padding for readability */
|
||||||
|
padding: 1rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smalltext {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: #111;
|
background: var(--sanic-header-background);
|
||||||
color: #e1e1e1;
|
color: var(--sanic-header-text);
|
||||||
border-bottom: 1px solid #272727;
|
border-bottom: 1px solid var(--sanic-header-border);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,20 +96,17 @@ footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin-top: 2rem;
|
margin: 2rem;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #88f;
|
color: var(--sanic-link);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
a:hover,
|
||||||
|
@ -62,18 +115,32 @@ a:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
|
||||||
height: 1.75rem;
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon {
|
span.icon {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#logo-simple {
|
||||||
|
height: 1.75rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
#logo-simple path:last-child {
|
||||||
background: #111;
|
fill: #e1e1e1;
|
||||||
color: #ccc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sanic pre,
|
||||||
|
#sanic code {
|
||||||
|
font-family: "Fira Code",
|
||||||
|
"Source Code Pro",
|
||||||
|
Menlo,
|
||||||
|
Meslo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
Lucida Console,
|
||||||
|
monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/** DirectoryPage **/
|
||||||
#breadcrumbs>a:hover {
|
#breadcrumbs>a:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
108
sanic/pages/styles/ErrorPage.css
Normal file
108
sanic/pages/styles/ErrorPage.css
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/** ErrorPage **/
|
||||||
|
#enduser {
|
||||||
|
max-width: 30em;
|
||||||
|
margin: 5em auto 5em auto;
|
||||||
|
text-align: justify;
|
||||||
|
/*text-justify: both;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#enduser a {
|
||||||
|
color: var(--sanic-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#enduser p:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
margin-top: 3em;
|
||||||
|
color: var(--sanic-text-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite {
|
||||||
|
--tracerite-var: var(--sanic-tracerite-var);
|
||||||
|
--tracerite-val: var(--sanic-tracerite-val);
|
||||||
|
--tracerite-type: var(--sanic-tracerite-type);
|
||||||
|
--tracerite-exception: var(--sanic);
|
||||||
|
--tracerite-highlight: var(--sanic-yellow);
|
||||||
|
--tracerite-tab: var(--sanic-tab-background);
|
||||||
|
--tracerite-tab-text: var(--sanic-tab-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite>h3 {
|
||||||
|
margin: 0.5rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic .tracerite .traceback-labels button {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 120%;
|
||||||
|
background: var(--tracerite-tab);
|
||||||
|
color: var(--tracerite-tab-text);
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-labels {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-labels button:hover {
|
||||||
|
filter: contrast(150%) brightness(120%) drop-shadow(0 -0 2px var(--sanic-tab-shadow));
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic .tracerite .tracerite-tooltip::before {
|
||||||
|
bottom: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic .tracerite .traceback-details mark span {
|
||||||
|
background: var(--sanic-highlight-background);
|
||||||
|
color: var(--sanic-highlight-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--sanic-header-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--sanic-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-value-display,
|
||||||
|
.exception-wrapper {
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-value-display {
|
||||||
|
background-color: var(--sanic-block-background);
|
||||||
|
color: var(--sanic-block-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-value-display h2 {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
grid-gap: .3em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table dt {
|
||||||
|
color: var(--sanic-block-alt-text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table dd {
|
||||||
|
/* Better breaking for cookies header and such */
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
|
@ -39,13 +39,13 @@ class Router(BaseRouter):
|
||||||
extra={"host": host} if host else None,
|
extra={"host": host} if host else None,
|
||||||
)
|
)
|
||||||
except RoutingNotFound as e:
|
except RoutingNotFound as e:
|
||||||
raise NotFound("Requested URL {} not found".format(e.path))
|
raise NotFound(f"Requested URL {e.path} not found") from None
|
||||||
except NoMethod as e:
|
except NoMethod as e:
|
||||||
raise MethodNotAllowed(
|
raise MethodNotAllowed(
|
||||||
"Method {} not allowed for URL {}".format(method, path),
|
f"Method {method} not allowed for URL {path}",
|
||||||
method=method,
|
method=method,
|
||||||
allowed_methods=e.allowed_methods,
|
allowed_methods=e.allowed_methods,
|
||||||
)
|
) from None
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||||
def get( # type: ignore
|
def get( # type: ignore
|
||||||
|
@ -61,6 +61,7 @@ class Router(BaseRouter):
|
||||||
correct response
|
correct response
|
||||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
return self._get(path, method, host)
|
return self._get(path, method, host)
|
||||||
|
|
||||||
def add( # type: ignore
|
def add( # type: ignore
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -112,6 +112,7 @@ requirements = [
|
||||||
"websockets>=10.0",
|
"websockets>=10.0",
|
||||||
"multidict>=5.0,<7.0",
|
"multidict>=5.0,<7.0",
|
||||||
"html5tagger>=1.2.1",
|
"html5tagger>=1.2.1",
|
||||||
|
"tracerite>=1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.errorpages import TextRenderer, guess_mime, exception_response
|
from sanic.errorpages import TextRenderer, exception_response, guess_mime
|
||||||
from sanic.exceptions import NotFound, SanicException
|
from sanic.exceptions import NotFound, SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
|
@ -57,7 +57,6 @@ def app():
|
||||||
raise Exception
|
raise Exception
|
||||||
return json({}) if param == "json" else html("")
|
return json({}) if param == "json" else html("")
|
||||||
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@ -314,7 +313,6 @@ def test_fallback_with_content_type_mismatch_accept(app):
|
||||||
("*/*", "application/json", "application/json"),
|
("*/*", "application/json", "application/json"),
|
||||||
# App wants text/plain but accept has equal entries for it
|
# App wants text/plain but accept has equal entries for it
|
||||||
("text/*,*/plain", None, "text/plain; charset=utf-8"),
|
("text/*,*/plain", None, "text/plain; charset=utf-8"),
|
||||||
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_combinations_for_auto(fake_request, accept, content_type, expected):
|
def test_combinations_for_auto(fake_request, accept, content_type, expected):
|
||||||
|
@ -428,25 +426,83 @@ def test_config_fallback_bad_value(app):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"route_format,fallback,accept,expected",
|
"route_format,fallback,accept,expected",
|
||||||
(
|
(
|
||||||
("json", "html", "*/*", "The client accepts */*, using 'json' from fakeroute"),
|
(
|
||||||
("json", "auto", "text/html,*/*;q=0.8", "The client accepts text/html, using 'html' from any"),
|
"json",
|
||||||
("json", "json", "text/html,*/*;q=0.8", "The client accepts */*;q=0.8, using 'json' from fakeroute"),
|
"html",
|
||||||
("", "html", "text/*,*/plain", "The client accepts text/*, using 'html' from FALLBACK_ERROR_FORMAT"),
|
"*/*",
|
||||||
("", "json", "text/*,*/*", "The client accepts */*, using 'json' from FALLBACK_ERROR_FORMAT"),
|
"The client accepts */*, using 'json' from fakeroute",
|
||||||
("", "auto", "*/*,application/json;q=0.5", "The client accepts */*, using 'json' from request.accept"),
|
),
|
||||||
("", "auto", "*/*", "The client accepts */*, using 'json' from content-type"),
|
(
|
||||||
("", "auto", "text/html,text/plain", "The client accepts text/plain, using 'text' from any"),
|
"json",
|
||||||
("", "auto", "text/html,text/plain;q=0.9", "The client accepts text/html, using 'html' from any"),
|
"auto",
|
||||||
("html", "json", "application/xml", "No format found, the client accepts [application/xml]"),
|
"text/html,*/*;q=0.8",
|
||||||
|
"The client accepts text/html, using 'html' from any",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"json",
|
||||||
|
"json",
|
||||||
|
"text/html,*/*;q=0.8",
|
||||||
|
"The client accepts */*;q=0.8, using 'json' from fakeroute",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"html",
|
||||||
|
"text/*,*/plain",
|
||||||
|
"The client accepts text/*, using 'html' from FALLBACK_ERROR_FORMAT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"json",
|
||||||
|
"text/*,*/*",
|
||||||
|
"The client accepts */*, using 'json' from FALLBACK_ERROR_FORMAT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"auto",
|
||||||
|
"*/*,application/json;q=0.5",
|
||||||
|
"The client accepts */*, using 'json' from request.accept",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"auto",
|
||||||
|
"*/*",
|
||||||
|
"The client accepts */*, using 'json' from content-type",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"auto",
|
||||||
|
"text/html,text/plain",
|
||||||
|
"The client accepts text/plain, using 'text' from any",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
"auto",
|
||||||
|
"text/html,text/plain;q=0.9",
|
||||||
|
"The client accepts text/html, using 'html' from any",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"html",
|
||||||
|
"json",
|
||||||
|
"application/xml",
|
||||||
|
"No format found, the client accepts [application/xml]",
|
||||||
|
),
|
||||||
("", "auto", "*/*", "The client accepts */*, using 'text' from any"),
|
("", "auto", "*/*", "The client accepts */*, using 'text' from any"),
|
||||||
("", "", "*/*", "No format found, the client accepts [*/*]"),
|
("", "", "*/*", "No format found, the client accepts [*/*]"),
|
||||||
# DEPRECATED: remove in 24.3
|
# DEPRECATED: remove in 24.3
|
||||||
("", "auto", "*/*", "The client accepts */*, using 'json' from request.json"),
|
(
|
||||||
|
"",
|
||||||
|
"auto",
|
||||||
|
"*/*",
|
||||||
|
"The client accepts */*, using 'json' from request.json",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_guess_mime_logging(caplog, fake_request, route_format, fallback, accept, expected):
|
def test_guess_mime_logging(
|
||||||
|
caplog, fake_request, route_format, fallback, accept, expected
|
||||||
|
):
|
||||||
class FakeObject:
|
class FakeObject:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fake_request.route = FakeObject()
|
fake_request.route = FakeObject()
|
||||||
fake_request.route.name = "fakeroute"
|
fake_request.route.name = "fakeroute"
|
||||||
fake_request.route.extra = FakeObject()
|
fake_request.route.extra = FakeObject()
|
||||||
|
@ -466,6 +522,8 @@ def test_guess_mime_logging(caplog, fake_request, route_format, fallback, accept
|
||||||
with caplog.at_level(logging.DEBUG, logger="sanic.root"):
|
with caplog.at_level(logging.DEBUG, logger="sanic.root"):
|
||||||
guess_mime(fake_request, fallback)
|
guess_mime(fake_request, fallback)
|
||||||
|
|
||||||
logmsg, = [r.message for r in caplog.records if r.funcName == "guess_mime"]
|
(logmsg,) = [
|
||||||
|
r.message for r in caplog.records if r.funcName == "guess_mime"
|
||||||
|
]
|
||||||
|
|
||||||
assert logmsg == expected
|
assert logmsg == expected
|
||||||
|
|
|
@ -23,11 +23,11 @@ from sanic.exceptions import (
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
def dl_to_dict(soup, css_class):
|
def dl_to_dict(soup, dl_id):
|
||||||
keys, values = [], []
|
keys, values = [], []
|
||||||
for dl in soup.find_all("dl", {"class": css_class}):
|
for dl in soup.find_all("dl", {"id": dl_id}):
|
||||||
for dt in dl.find_all("dt"):
|
for dt in dl.find_all("dt"):
|
||||||
keys.append(dt.text.strip())
|
keys.append(dt.text.split(":", 1)[0])
|
||||||
for dd in dl.find_all("dd"):
|
for dd in dl.find_all("dd"):
|
||||||
values.append(dd.text.strip())
|
values.append(dd.text.strip())
|
||||||
return dict(zip(keys, values))
|
return dict(zip(keys, values))
|
||||||
|
@ -194,10 +194,7 @@ def test_handled_unhandled_exception(exception_app):
|
||||||
assert "Internal Server Error" in soup.h1.text
|
assert "Internal Server Error" in soup.h1.text
|
||||||
|
|
||||||
message = " ".join(soup.p.text.split())
|
message = " ".join(soup.p.text.split())
|
||||||
assert message == (
|
assert "The application encountered an unexpected error" in message
|
||||||
"The server encountered an internal error and "
|
|
||||||
"cannot complete your request."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exception_in_exception_handler(exception_app):
|
def test_exception_in_exception_handler(exception_app):
|
||||||
|
@ -299,7 +296,7 @@ def test_contextual_exception_context(debug):
|
||||||
|
|
||||||
_, response = app.test_client.post("/coffee/html", debug=debug)
|
_, response = app.test_client.post("/coffee/html", debug=debug)
|
||||||
soup = BeautifulSoup(response.body, "html.parser")
|
soup = BeautifulSoup(response.body, "html.parser")
|
||||||
dl = dl_to_dict(soup, "context")
|
dl = dl_to_dict(soup, "exception-context")
|
||||||
assert response.status == 418
|
assert response.status == 418
|
||||||
assert "Sorry, I cannot brew coffee" in soup.find("p").text
|
assert "Sorry, I cannot brew coffee" in soup.find("p").text
|
||||||
assert dl == {"foo": "bar"}
|
assert dl == {"foo": "bar"}
|
||||||
|
@ -340,7 +337,7 @@ def test_contextual_exception_extra(debug):
|
||||||
|
|
||||||
_, response = app.test_client.post("/coffee/html", debug=debug)
|
_, response = app.test_client.post("/coffee/html", debug=debug)
|
||||||
soup = BeautifulSoup(response.body, "html.parser")
|
soup = BeautifulSoup(response.body, "html.parser")
|
||||||
dl = dl_to_dict(soup, "extra")
|
dl = dl_to_dict(soup, "exception-extra")
|
||||||
assert response.status == 418
|
assert response.status == 418
|
||||||
assert "Found bar" in soup.find("p").text
|
assert "Found bar" in soup.find("p").text
|
||||||
if debug:
|
if debug:
|
||||||
|
|
|
@ -123,10 +123,10 @@ def test_html_traceback_output_in_debug_mode(exception_handler_app: Sanic):
|
||||||
assert "handler_4" in html
|
assert "handler_4" in html
|
||||||
assert "foo = bar" in html
|
assert "foo = bar" in html
|
||||||
|
|
||||||
summary_text = " ".join(soup.select(".summary")[0].text.split())
|
summary_text = soup.select("h3")[0].text
|
||||||
assert (
|
assert "NameError: name 'bar' is not defined" == summary_text
|
||||||
"NameError: name 'bar' is not defined while handling path /4"
|
request_text = soup.select("h2")[-1].text
|
||||||
) == summary_text
|
assert "GET /4" == request_text
|
||||||
|
|
||||||
|
|
||||||
def test_inherited_exception_handler(exception_handler_app: Sanic):
|
def test_inherited_exception_handler(exception_handler_app: Sanic):
|
||||||
|
@ -146,11 +146,10 @@ def test_chained_exception_handler(exception_handler_app: Sanic):
|
||||||
assert "handler_6" in html
|
assert "handler_6" in html
|
||||||
assert "foo = 1 / arg" in html
|
assert "foo = 1 / arg" in html
|
||||||
assert "ValueError" in html
|
assert "ValueError" in html
|
||||||
|
assert "GET /6" in html
|
||||||
|
|
||||||
summary_text = " ".join(soup.select(".summary")[0].text.split())
|
summary_text = soup.select("h3")[0].text
|
||||||
assert (
|
assert "ZeroDivisionError: division by zero" == summary_text
|
||||||
"ZeroDivisionError: division by zero while handling path /6/0"
|
|
||||||
) == summary_text
|
|
||||||
|
|
||||||
|
|
||||||
def test_exception_handler_lookup(exception_handler_app: Sanic):
|
def test_exception_handler_lookup(exception_handler_app: Sanic):
|
||||||
|
|
|
@ -50,7 +50,7 @@ def raised_ceiling():
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'form-data; name="foo"; value="%22\\%0D%0A"',
|
'form-data; name="foo"; value="%22\\%0D%0A"',
|
||||||
("form-data", {"name": "foo", "value": '\"\\\n'})
|
("form-data", {"name": "foo", "value": '"\\\n'}),
|
||||||
),
|
),
|
||||||
# <input type=file name="foo";bar\"> with Unicode filename!
|
# <input type=file name="foo";bar\"> with Unicode filename!
|
||||||
(
|
(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user