From a5d7d034137dda16a81bc709eeac4a3ccea61db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=2E=20K=C3=A4rkk=C3=A4inen?= <98187+Tronic@users.noreply.github.com> Date: Mon, 6 Mar 2023 19:24:12 +0000 Subject: [PATCH] Nicer traceback formatting (#2667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com> Co-authored-by: Adam Hopkins Co-authored-by: L. Karkkainen Co-authored-by: SML --- pyproject.toml | 1 + sanic/app.py | 2 + sanic/application/logo.py | 2 +- sanic/errorpages.py | 140 +++------------------------ sanic/pages/base.py | 31 ++++-- sanic/pages/css.py | 4 +- sanic/pages/error.py | 109 +++++++++++++++++++++ sanic/pages/styles/BasePage.css | 109 +++++++++++++++++---- sanic/pages/styles/DirectoryPage.css | 1 + sanic/pages/styles/ErrorPage.css | 108 +++++++++++++++++++++ sanic/router.py | 7 +- setup.py | 1 + tests/test_errorpages.py | 90 ++++++++++++++--- tests/test_exceptions.py | 15 ++- tests/test_exceptions_handler.py | 15 ++- tests/test_headers.py | 2 +- 16 files changed, 442 insertions(+), 195 deletions(-) create mode 100644 sanic/pages/error.py create mode 100644 sanic/pages/styles/ErrorPage.css 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 = """Sanic""" # 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 = ( - "
{exc_name}: {exc_value}
" - "
{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 = ( - "
{title}
" - "
{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! (