diff --git a/pyproject.toml b/pyproject.toml
index a565de0f..ec20768d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,5 +24,6 @@ module = [
"sanic_routing.*",
"aioquic.*",
"html5tagger.*",
+ "tracerite.*",
]
ignore_missing_imports = true
diff --git a/sanic/app.py b/sanic/app.py
index bae5a36e..172ed877 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -875,6 +875,8 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
:param request: HTTP Request object
:return: Nothing
"""
+ __tracebackhide__ = True
+
await self.dispatch(
"http.lifecycle.handle",
inline=True,
diff --git a/sanic/application/logo.py b/sanic/application/logo.py
index a0211174..d3b0038e 100644
--- a/sanic/application/logo.py
+++ b/sanic/application/logo.py
@@ -40,7 +40,7 @@ FULL_COLOR_LOGO = """
""" # noqa
-SVG_LOGO = """""" # noqa
+SVG_LOGO_SIMPLE = """""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
diff --git a/sanic/errorpages.py b/sanic/errorpages.py
index 77b1a3b7..19ccc69b 100644
--- a/sanic/errorpages.py
+++ b/sanic/errorpages.py
@@ -23,6 +23,7 @@ from traceback import extract_tb
from sanic.exceptions import BadRequest, SanicException
from sanic.helpers import STATUS_CODES
from sanic.log import deprecation, logger
+from sanic.pages.error import ErrorPage
from sanic.response import html, json, text
@@ -38,10 +39,9 @@ if t.TYPE_CHECKING:
from sanic import HTTPResponse, Request
DEFAULT_FORMAT = "auto"
-FALLBACK_TEXT = (
- "The server encountered an internal error and "
- "cannot complete your request."
-)
+FALLBACK_TEXT = """\
+The application encountered an unexpected error and could not continue.\
+"""
FALLBACK_STATUS = 500
JSON = "application/json"
@@ -117,134 +117,18 @@ class HTMLRenderer(BaseRenderer):
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 = (
- "
"
- "{frame_html}
"
- )
- TRACEBACK_BORDER = (
- ""
- "The above exception was the direct cause of the following exception:"
- "
"
- )
- TRACEBACK_LINE_HTML = (
- ""
- "
"
- "File {0.filename}, line {0.lineno}, "
- "in {0.name}
"
- "
{0.line}
"
- "
"
- )
- OBJECT_WRAPPER_HTML = (
- ""
- "{display_html}
"
- )
- OBJECT_DISPLAY_HTML = "{key}{value}
"
- OUTPUT_HTML = (
- ""
- "{title}\n"
- "\n"
- "{title}
{text}\n"
- "{body}"
- )
-
def full(self) -> HTTPResponse:
- return html(
- self.OUTPUT_HTML.format(
- title=self.title,
- text=self.text,
- style=self.TRACEBACK_STYLE,
- body=self._generate_body(full=True),
- ),
- status=self.status,
+ page = ErrorPage(
+ debug=self.debug,
+ title=super().title,
+ text=super().text,
+ request=self.request,
+ exc=self.exception,
)
+ return html(page.render(), status=self.status, headers=self.headers)
def minimal(self) -> HTTPResponse:
- return html(
- 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"
Traceback of {appname} " "(most recent call last):
",
- f"{traceback_html}",
- "",
- f"{name}: {value} "
- f"while handling path {path}
",
- "
",
- ]
-
- 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,
- )
+ return self.full()
class TextRenderer(BaseRenderer):
diff --git a/sanic/pages/base.py b/sanic/pages/base.py
index 40a4e079..5be40345 100644
--- a/sanic/pages/base.py
+++ b/sanic/pages/base.py
@@ -1,18 +1,19 @@
from abc import ABC, abstractmethod
-from html5tagger import HTML, Document
+from html5tagger import HTML, Builder, Document
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
class BasePage(ABC, metaclass=CSS): # no cov
- TITLE = "Unknown"
+ TITLE = "Sanic"
+ HEADING = None
CSS: str
+ doc: Builder
def __init__(self, debug: bool = True) -> None:
- self.doc = Document(self.TITLE, lang="en")
self.debug = debug
@property
@@ -20,6 +21,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
return self.CSS
def render(self) -> str:
+ self.doc = Document(self.TITLE, lang="en", id="sanic")
self._head()
self._body()
self._foot()
@@ -28,7 +30,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
def _head(self) -> None:
self.doc.style(HTML(self.style))
with self.doc.header:
- self.doc.div(self.TITLE)
+ self.doc.div(self.HEADING or self.TITLE)
def _foot(self) -> None:
with self.doc.footer:
@@ -37,6 +39,23 @@ class BasePage(ABC, metaclass=CSS): # no cov
self._sanic_logo()
if self.debug:
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
def _body(self) -> None:
@@ -44,7 +63,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
def _sanic_logo(self) -> None:
self.doc.a(
- HTML(SVG_LOGO),
+ HTML(SVG_LOGO_SIMPLE),
href="https://sanic.dev",
target="_blank",
referrerpolicy="no-referrer",
diff --git a/sanic/pages/css.py b/sanic/pages/css.py
index ce698de0..8852d31a 100644
--- a/sanic/pages/css.py
+++ b/sanic/pages/css.py
@@ -24,8 +24,8 @@ class CSS(ABCMeta):
def __new__(cls, name, bases, attrs):
Page = super().__new__(cls, name, bases, attrs)
# Use a locally defined STYLE or the one from styles directory
- s = _extract_style(attrs.get("STYLE"), name)
- Page.STYLE = f"\n/* {name} */\n{s.strip()}\n" if s else ""
+ Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name)
+ Page.STYLE += attrs.get("STYLE_APPEND", "")
# Combine with all ancestor styles
Page.CSS = "".join(
Class.STYLE
diff --git a/sanic/pages/error.py b/sanic/pages/error.py
new file mode 100644
index 00000000..f1f5ed14
--- /dev/null
+++ b/sanic/pages/error.py
@@ -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
+ )
diff --git a/sanic/pages/styles/BasePage.css b/sanic/pages/styles/BasePage.css
index 431a7b44..d914bf2a 100644
--- a/sanic/pages/styles/BasePage.css
+++ b/sanic/pages/styles/BasePage.css
@@ -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 {
font: 16px sans-serif;
- background: #eee;
- color: #111;
+ background: var(--sanic-background);
+ color: var(--sanic-text);
+ scrollbar-gutter: stable;
+ overflow: hidden auto;
}
body {
margin: 0;
font-size: 1.25rem;
+ line-height: 125%;
}
body>* {
padding: 1rem 2vw;
}
-@media (max-width: 1200px) {
+@media (max-width: 1000px) {
body>* {
padding: 0.5rem 1.5vw;
}
- body {
- font-size: 1rem;
+ html {
+ /* 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 {
min-width: 600px;
max-width: 1600px;
}
header {
- background: #111;
- color: #e1e1e1;
- border-bottom: 1px solid #272727;
+ background: var(--sanic-header-background);
+ color: var(--sanic-header-text);
+ border-bottom: 1px solid var(--sanic-header-border);
text-align: center;
}
@@ -40,20 +96,17 @@ footer {
display: flex;
flex-direction: column;
font-size: 0.8rem;
- margin-top: 2rem;
+ margin: 2rem;
+ line-height: 1.5em;
}
h1 {
text-align: left;
}
-a:visited {
- color: inherit;
-}
-
a {
text-decoration: none;
- color: #88f;
+ color: var(--sanic-link);
}
a:hover,
@@ -62,18 +115,32 @@ a:focus {
outline: none;
}
-#logo {
- height: 1.75rem;
- padding: 0 0.25rem;
-}
span.icon {
margin-right: 1rem;
}
+#logo-simple {
+ height: 1.75rem;
+ padding: 0 0.25rem;
+}
+
+
@media (prefers-color-scheme: dark) {
- html {
- background: #111;
- color: #ccc;
+ #logo-simple path:last-child {
+ fill: #e1e1e1;
}
}
+
+#sanic pre,
+#sanic code {
+ font-family: "Fira Code",
+ "Source Code Pro",
+ Menlo,
+ Meslo,
+ Monaco,
+ Consolas,
+ Lucida Console,
+ monospace;
+ font-size: 0.8rem;
+}
diff --git a/sanic/pages/styles/DirectoryPage.css b/sanic/pages/styles/DirectoryPage.css
index c30e1e42..1f052afe 100644
--- a/sanic/pages/styles/DirectoryPage.css
+++ b/sanic/pages/styles/DirectoryPage.css
@@ -1,3 +1,4 @@
+/** DirectoryPage **/
#breadcrumbs>a:hover {
text-decoration: none;
}
diff --git a/sanic/pages/styles/ErrorPage.css b/sanic/pages/styles/ErrorPage.css
new file mode 100644
index 00000000..1fd27d12
--- /dev/null
+++ b/sanic/pages/styles/ErrorPage.css
@@ -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;
+}
diff --git a/sanic/router.py b/sanic/router.py
index b68fde3b..26d5eff6 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -39,13 +39,13 @@ class Router(BaseRouter):
extra={"host": host} if host else None,
)
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:
raise MethodNotAllowed(
- "Method {} not allowed for URL {}".format(method, path),
+ f"Method {method} not allowed for URL {path}",
method=method,
allowed_methods=e.allowed_methods,
- )
+ ) from None
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def get( # type: ignore
@@ -61,6 +61,7 @@ class Router(BaseRouter):
correct response
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
"""
+ __tracebackhide__ = True
return self._get(path, method, host)
def add( # type: ignore
diff --git a/setup.py b/setup.py
index 52332636..de142068 100644
--- a/setup.py
+++ b/setup.py
@@ -112,6 +112,7 @@ requirements = [
"websockets>=10.0",
"multidict>=5.0,<7.0",
"html5tagger>=1.2.1",
+ "tracerite>=1.0.0",
]
tests_require = [
diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py
index 538acf1b..40bdfcf8 100644
--- a/tests/test_errorpages.py
+++ b/tests/test_errorpages.py
@@ -4,7 +4,7 @@ import pytest
from sanic import Sanic
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.handlers import ErrorHandler
from sanic.request import Request
@@ -57,7 +57,6 @@ def app():
raise Exception
return json({}) if param == "json" else html("")
-
return app
@@ -314,7 +313,6 @@ def test_fallback_with_content_type_mismatch_accept(app):
("*/*", "application/json", "application/json"),
# App wants text/plain but accept has equal entries for it
("text/*,*/plain", None, "text/plain; charset=utf-8"),
-
),
)
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(
"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", "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]"),
+ (
+ "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",
+ "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"),
("", "", "*/*", "No format found, the client accepts [*/*]"),
# 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:
pass
+
fake_request.route = FakeObject()
fake_request.route.name = "fakeroute"
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"):
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
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 12008ee9..0fe51f8c 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -23,11 +23,11 @@ from sanic.exceptions import (
from sanic.response import text
-def dl_to_dict(soup, css_class):
+def dl_to_dict(soup, dl_id):
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"):
- keys.append(dt.text.strip())
+ keys.append(dt.text.split(":", 1)[0])
for dd in dl.find_all("dd"):
values.append(dd.text.strip())
return dict(zip(keys, values))
@@ -194,10 +194,7 @@ def test_handled_unhandled_exception(exception_app):
assert "Internal Server Error" in soup.h1.text
message = " ".join(soup.p.text.split())
- assert message == (
- "The server encountered an internal error and "
- "cannot complete your request."
- )
+ assert "The application encountered an unexpected error" in message
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)
soup = BeautifulSoup(response.body, "html.parser")
- dl = dl_to_dict(soup, "context")
+ dl = dl_to_dict(soup, "exception-context")
assert response.status == 418
assert "Sorry, I cannot brew coffee" in soup.find("p").text
assert dl == {"foo": "bar"}
@@ -340,7 +337,7 @@ def test_contextual_exception_extra(debug):
_, response = app.test_client.post("/coffee/html", debug=debug)
soup = BeautifulSoup(response.body, "html.parser")
- dl = dl_to_dict(soup, "extra")
+ dl = dl_to_dict(soup, "exception-extra")
assert response.status == 418
assert "Found bar" in soup.find("p").text
if debug:
diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py
index a3e43d3e..0c2ce40e 100644
--- a/tests/test_exceptions_handler.py
+++ b/tests/test_exceptions_handler.py
@@ -123,10 +123,10 @@ def test_html_traceback_output_in_debug_mode(exception_handler_app: Sanic):
assert "handler_4" in html
assert "foo = bar" in html
- summary_text = " ".join(soup.select(".summary")[0].text.split())
- assert (
- "NameError: name 'bar' is not defined while handling path /4"
- ) == summary_text
+ summary_text = soup.select("h3")[0].text
+ assert "NameError: name 'bar' is not defined" == summary_text
+ request_text = soup.select("h2")[-1].text
+ assert "GET /4" == request_text
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 "foo = 1 / arg" in html
assert "ValueError" in html
+ assert "GET /6" in html
- summary_text = " ".join(soup.select(".summary")[0].text.split())
- assert (
- "ZeroDivisionError: division by zero while handling path /6/0"
- ) == summary_text
+ summary_text = soup.select("h3")[0].text
+ assert "ZeroDivisionError: division by zero" == summary_text
def test_exception_handler_lookup(exception_handler_app: Sanic):
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 072357a9..fbf13d25 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -50,7 +50,7 @@ def raised_ceiling():
),
(
'form-data; name="foo"; value="%22\\%0D%0A"',
- ("form-data", {"name": "foo", "value": '\"\\\n'})
+ ("form-data", {"name": "foo", "value": '"\\\n'}),
),
# with Unicode filename!
(