Compare commits
16 Commits
sml-change
...
accept-enh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd2e4819d1 | ||
|
|
0e024b46d9 | ||
|
|
eae58e5d2a | ||
|
|
6472a69fbf | ||
|
|
2e2231919c | ||
|
|
8da10a9c0c | ||
|
|
ec25581262 | ||
|
|
b8ae4285a4 | ||
|
|
c0ca55530e | ||
|
|
52ecbb9dc7 | ||
|
|
3ef99568a5 | ||
|
|
dfe2148333 | ||
|
|
7909f673e5 | ||
|
|
e35286e332 | ||
|
|
8eeb1c20dc | ||
|
|
43c9a0a49b |
@@ -1 +1 @@
|
||||
__version__ = "23.3.0"
|
||||
__version__ = "22.12.0"
|
||||
|
||||
@@ -157,7 +157,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
"strict_slashes",
|
||||
"websocket_enabled",
|
||||
"websocket_tasks",
|
||||
"wrappers",
|
||||
)
|
||||
|
||||
_app_registry: Dict[str, "Sanic"] = {}
|
||||
@@ -876,8 +875,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
:param request: HTTP Request object
|
||||
:return: Nothing
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
await self.dispatch(
|
||||
"http.lifecycle.handle",
|
||||
inline=True,
|
||||
|
||||
@@ -40,7 +40,7 @@ FULL_COLOR_LOGO = """
|
||||
|
||||
""" # 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
|
||||
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
|
||||
|
||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
@@ -105,7 +105,6 @@ class Blueprint(BaseSanic):
|
||||
"version",
|
||||
"version_prefix",
|
||||
"websocket_routes",
|
||||
"wrappers",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -22,7 +22,6 @@ from traceback import extract_tb
|
||||
|
||||
from sanic.exceptions import BadRequest, SanicException
|
||||
from sanic.helpers import STATUS_CODES
|
||||
from sanic.pages.error import ErrorPage
|
||||
from sanic.response import html, json, text
|
||||
|
||||
|
||||
@@ -160,21 +159,36 @@ class HTMLRenderer(BaseRenderer):
|
||||
"{body}"
|
||||
)
|
||||
|
||||
def _page(self, full: bool) -> HTTPResponse:
|
||||
page = ErrorPage(
|
||||
title=super().title,
|
||||
text=super().text,
|
||||
request=self.request,
|
||||
exc=self.exception,
|
||||
full=full,
|
||||
)
|
||||
return html(page.render(), status=self.status, headers=self.headers)
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
return self._page(full=True)
|
||||
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,
|
||||
)
|
||||
|
||||
def minimal(self) -> HTTPResponse:
|
||||
return self._page(full=False)
|
||||
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 = []
|
||||
|
||||
296
sanic/headers.py
296
sanic/headers.py
@@ -35,141 +35,96 @@ _host_re = re.compile(
|
||||
|
||||
def parse_arg_as_accept(f):
|
||||
def func(self, other, *args, **kwargs):
|
||||
if not isinstance(other, Accept) and other:
|
||||
other = Accept.parse(other)
|
||||
if not isinstance(other, MediaType) and other:
|
||||
other = MediaType._parse(other)
|
||||
return f(self, other, *args, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class MediaType(str):
|
||||
def __new__(cls, value: str):
|
||||
return str.__new__(cls, value)
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
self.value = value
|
||||
self.is_wildcard = self.check_if_wildcard(value)
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.is_wildcard:
|
||||
return True
|
||||
|
||||
if self.match(other):
|
||||
return True
|
||||
|
||||
other_is_wildcard = (
|
||||
other.is_wildcard
|
||||
if isinstance(other, MediaType)
|
||||
else self.check_if_wildcard(other)
|
||||
)
|
||||
|
||||
return other_is_wildcard
|
||||
|
||||
def match(self, other):
|
||||
other_value = other.value if isinstance(other, MediaType) else other
|
||||
return self.value == other_value
|
||||
|
||||
@staticmethod
|
||||
def check_if_wildcard(value):
|
||||
return value == "*"
|
||||
|
||||
|
||||
class Accept(str):
|
||||
def __new__(cls, value: str, *args, **kwargs):
|
||||
return str.__new__(cls, value)
|
||||
class MediaType:
|
||||
"""A media type, as used in the Accept header."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: str,
|
||||
type_: MediaType,
|
||||
subtype: MediaType,
|
||||
*,
|
||||
q: str = "1.0",
|
||||
**kwargs: str,
|
||||
type_: str,
|
||||
subtype: str,
|
||||
**params: str,
|
||||
):
|
||||
qvalue = float(q)
|
||||
if qvalue > 1 or qvalue < 0:
|
||||
raise InvalidHeader(
|
||||
f"Accept header qvalue must be between 0 and 1, not: {qvalue}"
|
||||
)
|
||||
self.value = value
|
||||
self.type_ = type_
|
||||
self.subtype = subtype
|
||||
self.qvalue = qvalue
|
||||
self.params = kwargs
|
||||
self.q = float(params.get("q", "1.0"))
|
||||
self.params = params
|
||||
self.mime = f"{type_}/{subtype}"
|
||||
|
||||
def _compare(self, other, method):
|
||||
try:
|
||||
return method(self.qvalue, other.qvalue)
|
||||
except (AttributeError, TypeError):
|
||||
return NotImplemented
|
||||
def __repr__(self):
|
||||
return self.mime + "".join(f";{k}={v}" for k, v in self.params.items())
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __lt__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s < o)
|
||||
def __eq__(self, other):
|
||||
"""Check for mime (str or MediaType) identical type/subtype."""
|
||||
if isinstance(other, str):
|
||||
return self.mime == other
|
||||
if isinstance(other, MediaType):
|
||||
return self.mime == other.mime
|
||||
return NotImplemented
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __le__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s <= o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __eq__(self, other: Union[str, Accept]): # type: ignore
|
||||
return self._compare(other, lambda s, o: s == o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __ge__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s >= o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __gt__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s > o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __ne__(self, other: Union[str, Accept]): # type: ignore
|
||||
return self._compare(other, lambda s, o: s != o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def match(
|
||||
self,
|
||||
other,
|
||||
*,
|
||||
allow_type_wildcard: bool = True,
|
||||
allow_subtype_wildcard: bool = True,
|
||||
) -> bool:
|
||||
type_match = (
|
||||
self.type_ == other.type_
|
||||
if allow_type_wildcard
|
||||
else (
|
||||
self.type_.match(other.type_)
|
||||
and not self.type_.is_wildcard
|
||||
and not other.type_.is_wildcard
|
||||
)
|
||||
)
|
||||
subtype_match = (
|
||||
self.subtype == other.subtype
|
||||
if allow_subtype_wildcard
|
||||
else (
|
||||
self.subtype.match(other.subtype)
|
||||
and not self.subtype.is_wildcard
|
||||
and not other.subtype.is_wildcard
|
||||
mime: str,
|
||||
allow_type_wildcard=True,
|
||||
allow_subtype_wildcard=True,
|
||||
) -> Optional[MediaType]:
|
||||
"""Check if this media type matches the given mime type/subtype.
|
||||
|
||||
Wildcards are supported both ways on both type and subtype.
|
||||
|
||||
Note: Use the `==` operator instead to check for literal matches
|
||||
without expanding wildcards.
|
||||
|
||||
@param media_type: A type/subtype string to match.
|
||||
@return `self` if the media types are compatible, else `None`
|
||||
"""
|
||||
mt = MediaType._parse(mime)
|
||||
return (
|
||||
self
|
||||
if (
|
||||
# Subtype match
|
||||
(self.subtype in (mt.subtype, "*") or mt.subtype == "*")
|
||||
# Type match
|
||||
and (self.type_ in (mt.type_, "*") or mt.type_ == "*")
|
||||
# Allow disabling wildcards (backwards compatibility with tests)
|
||||
and (
|
||||
allow_type_wildcard
|
||||
or self.type_ != "*"
|
||||
and mt.type_ != "*"
|
||||
)
|
||||
and (
|
||||
allow_subtype_wildcard
|
||||
or self.subtype != "*"
|
||||
and mt.subtype != "*"
|
||||
)
|
||||
)
|
||||
else None
|
||||
)
|
||||
|
||||
return type_match and subtype_match
|
||||
@property
|
||||
def has_wildcard(self) -> bool:
|
||||
"""Return True if this media type has a wildcard in it."""
|
||||
return "*" in (self.subtype, self.type_)
|
||||
|
||||
@property
|
||||
def is_wildcard(self) -> bool:
|
||||
"""Return True if this is the wildcard `*/*`"""
|
||||
return self.type_ == "*" and self.subtype == "*"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw: str) -> Accept:
|
||||
invalid = False
|
||||
mtype = raw.strip()
|
||||
def _parse(cls, mime_with_params: str) -> MediaType:
|
||||
mtype = mime_with_params.strip()
|
||||
|
||||
try:
|
||||
media, *raw_params = mtype.split(";")
|
||||
type_, subtype = media.split("/")
|
||||
except ValueError:
|
||||
invalid = True
|
||||
|
||||
if invalid or not type_ or not subtype:
|
||||
raise InvalidHeader(f"Header contains invalid Accept value: {raw}")
|
||||
media, *raw_params = mtype.split(";")
|
||||
type_, subtype = media.split("/", 1)
|
||||
if not type_ or not subtype:
|
||||
raise ValueError(f"Invalid media type: {mtype}")
|
||||
|
||||
params = dict(
|
||||
[
|
||||
@@ -178,28 +133,79 @@ class Accept(str):
|
||||
]
|
||||
)
|
||||
|
||||
return cls(mtype, MediaType(type_), MediaType(subtype), **params)
|
||||
return cls(type_.lstrip(), subtype.rstrip(), **params)
|
||||
|
||||
|
||||
class AcceptContainer(list):
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return any(item.match(o) for item in self)
|
||||
class Matched(str):
|
||||
"""A matching result of a MIME string against a MediaType."""
|
||||
|
||||
def match(
|
||||
self,
|
||||
o: object,
|
||||
*,
|
||||
allow_type_wildcard: bool = True,
|
||||
allow_subtype_wildcard: bool = True,
|
||||
) -> bool:
|
||||
return any(
|
||||
item.match(
|
||||
o,
|
||||
allow_type_wildcard=allow_type_wildcard,
|
||||
allow_subtype_wildcard=allow_subtype_wildcard,
|
||||
)
|
||||
for item in self
|
||||
def __new__(cls, mime: str, m: Optional[MediaType]):
|
||||
return super().__new__(cls, mime)
|
||||
|
||||
def __init__(self, mime: str, m: Optional[MediaType]):
|
||||
self.m = m
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self} matched {self.m}>" if self else "<no match>"
|
||||
|
||||
|
||||
class AcceptList(list):
|
||||
"""A list of media types, as used in the Accept header.
|
||||
|
||||
The Accept header entries are listed in order of preference, starting
|
||||
with the most preferred. This class is a list of `MediaType` objects,
|
||||
that encapsulate also the q value or any other parameters.
|
||||
|
||||
Two separate methods are provided for searching the list:
|
||||
- 'match' for finding the most preferred match (wildcards supported)
|
||||
- operator 'in' for checking explicit matches (wildcards as literals)
|
||||
"""
|
||||
|
||||
def match(self, *mimes: str) -> Matched:
|
||||
"""Find a media type accepted by the client.
|
||||
|
||||
This method can be used to find which of the media types requested by
|
||||
the client is most preferred against the ones given as arguments.
|
||||
|
||||
The ordering of preference is set by:
|
||||
1. The q values on the Accept header, and those being equal,
|
||||
2. The order of the arguments (first is most preferred), and
|
||||
3. The first matching entry on the Accept header.
|
||||
|
||||
Wildcards are matched both ways. A match is usually found, as the
|
||||
Accept headers typically include `*/*`, in particular if the header
|
||||
is missing, is not manually set, or if the client is a browser.
|
||||
|
||||
Note: the returned object behaves as a string of the mime argument
|
||||
that matched, and is empty/falsy if no match was found. The matched
|
||||
header entry `MediaType` or `None` is available as the `m` attribute.
|
||||
|
||||
@param mimes: Any MIME types to search for in order of preference.
|
||||
@return A match object with the mime string and the MediaType object.
|
||||
"""
|
||||
l = sorted(
|
||||
[
|
||||
(-acc.q, i, j, mime, acc) # Sort by -q, i, j
|
||||
for j, acc in enumerate(self)
|
||||
for i, mime in enumerate(mimes)
|
||||
if acc.match(mime)
|
||||
]
|
||||
)
|
||||
return Matched(*(l[0][3:] if l else ("", None)))
|
||||
|
||||
|
||||
def parse_accept(accept: str) -> AcceptList:
|
||||
"""Parse an Accept header and order the acceptable media types in
|
||||
accorsing to RFC 7231, s. 5.3.2
|
||||
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
"""
|
||||
if not accept:
|
||||
return AcceptList()
|
||||
try:
|
||||
a = [MediaType._parse(mtype) for mtype in accept.split(",")]
|
||||
return AcceptList(sorted(a, key=lambda mtype: -mtype.q))
|
||||
except ValueError:
|
||||
raise InvalidHeader(f"Invalid header value in Accept: {accept}")
|
||||
|
||||
|
||||
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
@@ -368,34 +374,6 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||
return ret
|
||||
|
||||
|
||||
def _sort_accept_value(accept: Accept):
|
||||
return (
|
||||
accept.qvalue,
|
||||
len(accept.params),
|
||||
accept.subtype != "*",
|
||||
accept.type_ != "*",
|
||||
)
|
||||
|
||||
|
||||
def parse_accept(accept: str) -> AcceptContainer:
|
||||
"""Parse an Accept header and order the acceptable media types in
|
||||
accorsing to RFC 7231, s. 5.3.2
|
||||
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
"""
|
||||
media_types = accept.split(",")
|
||||
accept_list: List[Accept] = []
|
||||
|
||||
for mtype in media_types:
|
||||
if not mtype:
|
||||
continue
|
||||
|
||||
accept_list.append(Accept.parse(mtype))
|
||||
|
||||
return AcceptContainer(
|
||||
sorted(accept_list, key=_sort_accept_value, reverse=True)
|
||||
)
|
||||
|
||||
|
||||
def parse_credentials(
|
||||
header: Optional[str],
|
||||
prefixes: Union[List, Tuple, Set] = None,
|
||||
|
||||
@@ -14,7 +14,6 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_middleware: List[FutureMiddleware] = []
|
||||
self.wrappers = []
|
||||
|
||||
def _apply_middleware(self, middleware: FutureMiddleware):
|
||||
raise NotImplementedError # noqa
|
||||
@@ -141,7 +140,3 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
reverse=True,
|
||||
)[::-1]
|
||||
)
|
||||
|
||||
def wrap(self, handler):
|
||||
self.wrappers.append(handler)
|
||||
return handler
|
||||
|
||||
@@ -267,11 +267,11 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
if single_process and legacy:
|
||||
raise RuntimeError("Cannot run single process and legacy mode")
|
||||
|
||||
# if register_sys_signals is False and not (single_process or legacy):
|
||||
# raise RuntimeError(
|
||||
# "Cannot run Sanic.serve with register_sys_signals=False. "
|
||||
# "Use either Sanic.serve_single or Sanic.serve_legacy."
|
||||
# )
|
||||
if register_sys_signals is False and not (single_process or legacy):
|
||||
raise RuntimeError(
|
||||
"Cannot run Sanic.serve with register_sys_signals=False. "
|
||||
"Use either Sanic.serve_single or Sanic.serve_legacy."
|
||||
)
|
||||
|
||||
if motd_display:
|
||||
self.config.MOTD_DISPLAY.update(motd_display)
|
||||
|
||||
@@ -3,16 +3,16 @@ from abc import ABC, abstractmethod
|
||||
from html5tagger import HTML, Document
|
||||
|
||||
from sanic import __version__ as VERSION
|
||||
from sanic.application.logo import SVG_LOGO_SIMPLE
|
||||
from sanic.application.logo import SVG_LOGO
|
||||
from sanic.pages.css import CSS
|
||||
|
||||
|
||||
class BasePage(ABC, metaclass=CSS): # no cov
|
||||
TITLE = "Sanic"
|
||||
TITLE = "Unknown"
|
||||
CSS: str
|
||||
|
||||
def __init__(self, debug: bool = True) -> None:
|
||||
self.doc = None
|
||||
self.doc = Document(self.TITLE, lang="en")
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
@@ -20,7 +20,6 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||
return self.CSS
|
||||
|
||||
def render(self) -> str:
|
||||
self.doc = Document(self.TITLE, lang="en")
|
||||
self._head()
|
||||
self._body()
|
||||
self._foot()
|
||||
@@ -45,7 +44,7 @@ class BasePage(ABC, metaclass=CSS): # no cov
|
||||
|
||||
def _sanic_logo(self) -> None:
|
||||
self.doc.a(
|
||||
HTML(SVG_LOGO_SIMPLE),
|
||||
HTML(SVG_LOGO),
|
||||
href="https://sanic.dev",
|
||||
target="_blank",
|
||||
referrerpolicy="no-referrer",
|
||||
|
||||
@@ -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
|
||||
Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name)
|
||||
Page.STYLE += attrs.get("STYLE_APPEND", "")
|
||||
s = _extract_style(attrs.get("STYLE"), name)
|
||||
Page.STYLE = f"\n/* {name} */\n{s.strip()}\n" if s else ""
|
||||
# Combine with all ancestor styles
|
||||
Page.CSS = "".join(
|
||||
Class.STYLE
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
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.""" # noqa: E501
|
||||
|
||||
|
||||
class ErrorPage(BasePage):
|
||||
STYLE_APPEND = tracerite.html.style
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
text: str,
|
||||
request: Request,
|
||||
exc: Exception,
|
||||
full: bool,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
# Internal server errors come with the text of the exception,
|
||||
# which we don't want to show to the user.
|
||||
# FIXME: Needs to be done in a better way, elsewhere
|
||||
if "Internal Server Error" in title:
|
||||
text = "The application encountered an unexpected error and could not continue." # noqa: E501
|
||||
name = request.app.name.replace("_", " ").strip()
|
||||
if name.islower():
|
||||
name = name.title()
|
||||
self.TITLE = E("Application ").strong(name)(
|
||||
" cannot handle your request"
|
||||
)
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.request = request
|
||||
self.exc = exc
|
||||
self.full = full
|
||||
|
||||
def _head(self) -> None:
|
||||
self.doc._script(tracerite.html.javascript)
|
||||
super()._head()
|
||||
|
||||
def _body(self) -> None:
|
||||
debug = self.request.app.debug
|
||||
try:
|
||||
route_name = self.request.route.name
|
||||
except AttributeError:
|
||||
route_name = "[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.full, 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:
|
||||
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:
|
||||
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,76 +1,28 @@
|
||||
/** BasePage **/
|
||||
|
||||
:root {
|
||||
--sanic: #ff0d68;
|
||||
--sanic-blue: #0092FF;
|
||||
--sanic-yellow: #FFE900;
|
||||
--sanic-purple: #833FE3;
|
||||
--sanic-green: #37ae6f;
|
||||
--sanic-background: #f1f5f9;
|
||||
--sanic-text: #1f2937;
|
||||
--sanic-tab-background: #fff;
|
||||
--sanic-tab-text: #0f172a;
|
||||
--sanic-tab-shadow: #adadad;
|
||||
--sanic-highlight-background: var(--sanic-yellow);
|
||||
--sanic-highlight-text: var(--sanic-text);
|
||||
--sanic-header-background: #000;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--sanic-purple: #D246DE;
|
||||
--sanic-green: #16DB93;
|
||||
--sanic-background: #111;
|
||||
--sanic-text: #e7e7e7;
|
||||
--sanic-tab-background: #484848;
|
||||
--sanic-tab-text: #e1e1e1;
|
||||
--sanic-tab-shadow: #000;
|
||||
--sanic-highlight-background: var(--sanic-yellow);
|
||||
--sanic-highlight-text: #000;
|
||||
--sanic-header-background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font: 16px sans-serif;
|
||||
background: var(--sanic-background);
|
||||
color: var(--sanic-text);
|
||||
scrollbar-gutter: stable;
|
||||
overflow: hidden auto;
|
||||
background: #eee;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 125%;
|
||||
}
|
||||
|
||||
body>* {
|
||||
padding: 1rem 2vw;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 1200px) {
|
||||
body>* {
|
||||
padding: 0.5rem 1.5vw;
|
||||
}
|
||||
|
||||
html {
|
||||
/* Scale everything by rem of 6px-16px by viewport width */
|
||||
font-size: calc(6px + 10 * 100vw / 1000);
|
||||
body {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 70vh;
|
||||
/* Make sure the footer is closer to bottom */
|
||||
padding: 1rem 2.5rem;
|
||||
/* Generous padding for readability */
|
||||
}
|
||||
|
||||
.smalltext {
|
||||
font-size: 1.0rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 600px;
|
||||
max-width: 1600px;
|
||||
@@ -110,19 +62,18 @@ 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) {
|
||||
#logo-simple path:last-child {
|
||||
fill: #e1e1e1;
|
||||
html {
|
||||
background: #111;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/** DirectoryPage **/
|
||||
#breadcrumbs>a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/** 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-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tracerite {
|
||||
--tracerite-var: var(--sanic-blue);
|
||||
--tracerite-val: var(--sanic-text);
|
||||
--tracerite-type: var(--sanic-green);
|
||||
--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;
|
||||
}
|
||||
|
||||
.tracerite .traceback-labels button {
|
||||
font-size: 0.8rem !important;
|
||||
line-height: 120% !important;
|
||||
background: var(--tracerite-tab) !important;
|
||||
color: var(--tracerite-tab-text) !important;
|
||||
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));
|
||||
}
|
||||
|
||||
.tracerite .traceback-details mark span {
|
||||
background: var(--sanic-highlight-background) !important;
|
||||
color: var(--sanic-highlight-text) !important;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--sanic-header-background);
|
||||
}
|
||||
|
||||
h1 {
|
||||
/*margin-left: -1.5rem; !* Emoji partially in the left margin *!*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 1em 0 0.2em 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--sanic-text);
|
||||
}
|
||||
|
||||
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: #888;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
dl.key-value-table dd {
|
||||
word-break: break-all;
|
||||
/* Better breaking for cookies header and such */
|
||||
}
|
||||
|
||||
.tracerite .codeline {
|
||||
font-family:
|
||||
"Fira Code",
|
||||
"Source Code Pro",
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
Lucida Console,
|
||||
monospace;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ from sanic.constants import (
|
||||
)
|
||||
from sanic.exceptions import BadRequest, BadURL, ServerError
|
||||
from sanic.headers import (
|
||||
AcceptContainer,
|
||||
AcceptList,
|
||||
Options,
|
||||
parse_accept,
|
||||
parse_content_header,
|
||||
@@ -167,7 +167,7 @@ class Request:
|
||||
self.conn_info: Optional[ConnInfo] = None
|
||||
self.ctx = SimpleNamespace()
|
||||
self.parsed_forwarded: Optional[Options] = None
|
||||
self.parsed_accept: Optional[AcceptContainer] = None
|
||||
self.parsed_accept: Optional[AcceptList] = None
|
||||
self.parsed_credentials: Optional[Credentials] = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form: Optional[RequestParameters] = None
|
||||
@@ -499,7 +499,7 @@ class Request:
|
||||
return self.parsed_json
|
||||
|
||||
@property
|
||||
def accept(self) -> AcceptContainer:
|
||||
def accept(self) -> AcceptList:
|
||||
"""
|
||||
:return: The ``Accept`` header parsed
|
||||
:rtype: AcceptContainer
|
||||
|
||||
@@ -39,13 +39,13 @@ class Router(BaseRouter):
|
||||
extra={"host": host} if host else None,
|
||||
)
|
||||
except RoutingNotFound as e:
|
||||
raise NotFound(f"Requested URL {e.path} not found") from None
|
||||
raise NotFound("Requested URL {} not found".format(e.path))
|
||||
except NoMethod as e:
|
||||
raise MethodNotAllowed(
|
||||
f"Method {method} not allowed for URL {path}",
|
||||
"Method {} not allowed for URL {}".format(method, path),
|
||||
method=method,
|
||||
allowed_methods=e.allowed_methods,
|
||||
) from None
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def get( # type: ignore
|
||||
@@ -61,7 +61,6 @@ class Router(BaseRouter):
|
||||
correct response
|
||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
return self._get(path, method, host)
|
||||
|
||||
def add( # type: ignore
|
||||
|
||||
@@ -130,14 +130,13 @@ def _setup_system_signals(
|
||||
register_sys_signals: bool,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
) -> None: # no cov
|
||||
print(">>>>>>>>>>>>>>>>>.", run_multiple)
|
||||
# Ignore SIGINT when run_multiple
|
||||
if run_multiple:
|
||||
signal_func(SIGINT, SIG_IGN)
|
||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals and False:
|
||||
if register_sys_signals:
|
||||
if OS_IS_WINDOWS:
|
||||
ctrlc_workaround_for_windows(app)
|
||||
else:
|
||||
|
||||
1
setup.py
1
setup.py
@@ -112,7 +112,6 @@ requirements = [
|
||||
"websockets>=10.0",
|
||||
"multidict>=5.0,<7.0",
|
||||
"html5tagger>=1.2.1",
|
||||
"tracerite>=1.0.0",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
|
||||
@@ -185,30 +185,22 @@ def test_request_line(app):
|
||||
|
||||
assert request.request_line == b"GET / HTTP/1.1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw",
|
||||
(
|
||||
"show/first, show/second",
|
||||
"show/*, show/first",
|
||||
"*/*, show/first",
|
||||
"*/*, show/*",
|
||||
"other/*; q=0.1, show/*; q=0.2",
|
||||
"show/first; q=0.5, show/second; q=0.5",
|
||||
"show/first; foo=bar, show/second; foo=bar",
|
||||
"show/second, show/first; foo=bar",
|
||||
"show/second; q=0.5, show/first; foo=bar; q=0.5",
|
||||
"show/second; q=0.5, show/first; q=1.0",
|
||||
"show/first, show/second; q=1.0",
|
||||
),
|
||||
)
|
||||
def test_parse_accept_ordered_okay(raw):
|
||||
ordered = headers.parse_accept(raw)
|
||||
expected_subtype = (
|
||||
"*" if all(q.subtype.is_wildcard for q in ordered) else "first"
|
||||
"text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8",
|
||||
"application/xml;q=0.9, */*;q=0.8, text/html, application/xhtml+xml",
|
||||
"foo/bar;q=0.9, */*;q=0.8, text/html=0.8, text/plain, application/xhtml+xml",
|
||||
)
|
||||
assert ordered[0].type_ == "show"
|
||||
assert ordered[0].subtype == expected_subtype
|
||||
)
|
||||
def test_accept_ordering(raw):
|
||||
"""Should sort by q but also be stable."""
|
||||
accept = headers.parse_accept(raw)
|
||||
assert accept[0].type_ == "text"
|
||||
raw1 = ", ".join(str(a) for a in accept)
|
||||
accept = headers.parse_accept(raw1)
|
||||
raw2 = ", ".join(str(a) for a in accept)
|
||||
assert raw1 == raw2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -225,40 +217,27 @@ def test_bad_accept(raw):
|
||||
|
||||
|
||||
def test_empty_accept():
|
||||
assert headers.parse_accept("") == []
|
||||
a = headers.parse_accept("")
|
||||
assert a == []
|
||||
assert not a.match("*/*")
|
||||
|
||||
|
||||
def test_wildcard_accept_set_ok():
|
||||
accept = headers.parse_accept("*/*")[0]
|
||||
assert accept.type_.is_wildcard
|
||||
assert accept.subtype.is_wildcard
|
||||
assert accept.is_wildcard
|
||||
assert accept.has_wildcard
|
||||
|
||||
accept = headers.parse_accept("foo/*")[0]
|
||||
assert not accept.is_wildcard
|
||||
assert accept.has_wildcard
|
||||
|
||||
accept = headers.parse_accept("*/bar")[0]
|
||||
assert not accept.is_wildcard
|
||||
assert accept.has_wildcard
|
||||
|
||||
accept = headers.parse_accept("foo/bar")[0]
|
||||
assert not accept.type_.is_wildcard
|
||||
assert not accept.subtype.is_wildcard
|
||||
|
||||
|
||||
def test_accept_parsed_against_str():
|
||||
accept = headers.Accept.parse("foo/bar")
|
||||
assert accept > "foo/bar; q=0.1"
|
||||
|
||||
|
||||
def test_media_type_equality():
|
||||
assert headers.MediaType("foo") == headers.MediaType("foo") == "foo"
|
||||
assert headers.MediaType("foo") == headers.MediaType("*") == "*"
|
||||
assert headers.MediaType("foo") != headers.MediaType("bar")
|
||||
assert headers.MediaType("foo") != "bar"
|
||||
|
||||
|
||||
def test_media_type_matching():
|
||||
assert headers.MediaType("foo").match(headers.MediaType("foo"))
|
||||
assert headers.MediaType("foo").match("foo")
|
||||
|
||||
assert not headers.MediaType("foo").match(headers.MediaType("*"))
|
||||
assert not headers.MediaType("foo").match("*")
|
||||
|
||||
assert not headers.MediaType("foo").match(headers.MediaType("bar"))
|
||||
assert not headers.MediaType("foo").match("bar")
|
||||
assert not accept.is_wildcard
|
||||
assert not accept.has_wildcard
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -266,87 +245,52 @@ def test_media_type_matching():
|
||||
(
|
||||
# ALLOW BOTH
|
||||
("foo/bar", "foo/bar", True, True, True),
|
||||
("foo/bar", headers.Accept.parse("foo/bar"), True, True, True),
|
||||
("foo/bar", "foo/*", True, True, True),
|
||||
("foo/bar", headers.Accept.parse("foo/*"), True, True, True),
|
||||
("foo/bar", "*/*", True, True, True),
|
||||
("foo/bar", headers.Accept.parse("*/*"), True, True, True),
|
||||
("foo/*", "foo/bar", True, True, True),
|
||||
("foo/*", headers.Accept.parse("foo/bar"), True, True, True),
|
||||
("foo/*", "foo/*", True, True, True),
|
||||
("foo/*", headers.Accept.parse("foo/*"), True, True, True),
|
||||
("foo/*", "*/*", True, True, True),
|
||||
("foo/*", headers.Accept.parse("*/*"), True, True, True),
|
||||
("*/*", "foo/bar", True, True, True),
|
||||
("*/*", headers.Accept.parse("foo/bar"), True, True, True),
|
||||
("*/*", "foo/*", True, True, True),
|
||||
("*/*", headers.Accept.parse("foo/*"), True, True, True),
|
||||
("*/*", "*/*", True, True, True),
|
||||
("*/*", headers.Accept.parse("*/*"), True, True, True),
|
||||
# ALLOW TYPE
|
||||
("foo/bar", "foo/bar", True, True, False),
|
||||
("foo/bar", headers.Accept.parse("foo/bar"), True, True, False),
|
||||
("foo/bar", "foo/*", False, True, False),
|
||||
("foo/bar", headers.Accept.parse("foo/*"), False, True, False),
|
||||
("foo/bar", "*/*", False, True, False),
|
||||
("foo/bar", headers.Accept.parse("*/*"), False, True, False),
|
||||
("foo/*", "foo/bar", False, True, False),
|
||||
("foo/*", headers.Accept.parse("foo/bar"), False, True, False),
|
||||
("foo/*", "foo/*", False, True, False),
|
||||
("foo/*", headers.Accept.parse("foo/*"), False, True, False),
|
||||
("foo/*", "*/*", False, True, False),
|
||||
("foo/*", headers.Accept.parse("*/*"), False, True, False),
|
||||
("*/*", "foo/bar", False, True, False),
|
||||
("*/*", headers.Accept.parse("foo/bar"), False, True, False),
|
||||
("*/*", "foo/*", False, True, False),
|
||||
("*/*", headers.Accept.parse("foo/*"), False, True, False),
|
||||
("*/*", "*/*", False, True, False),
|
||||
("*/*", headers.Accept.parse("*/*"), False, True, False),
|
||||
# ALLOW SUBTYPE
|
||||
("foo/bar", "foo/bar", True, False, True),
|
||||
("foo/bar", headers.Accept.parse("foo/bar"), True, False, True),
|
||||
("foo/bar", "foo/*", True, False, True),
|
||||
("foo/bar", headers.Accept.parse("foo/*"), True, False, True),
|
||||
("foo/bar", "*/*", False, False, True),
|
||||
("foo/bar", headers.Accept.parse("*/*"), False, False, True),
|
||||
("foo/*", "foo/bar", True, False, True),
|
||||
("foo/*", headers.Accept.parse("foo/bar"), True, False, True),
|
||||
("foo/*", "foo/*", True, False, True),
|
||||
("foo/*", headers.Accept.parse("foo/*"), True, False, True),
|
||||
("foo/*", "*/*", False, False, True),
|
||||
("foo/*", headers.Accept.parse("*/*"), False, False, True),
|
||||
("*/*", "foo/bar", False, False, True),
|
||||
("*/*", headers.Accept.parse("foo/bar"), False, False, True),
|
||||
("*/*", "foo/*", False, False, True),
|
||||
("*/*", headers.Accept.parse("foo/*"), False, False, True),
|
||||
("*/*", "*/*", False, False, True),
|
||||
("*/*", headers.Accept.parse("*/*"), False, False, True),
|
||||
),
|
||||
)
|
||||
def test_accept_matching(value, other, outcome, allow_type, allow_subtype):
|
||||
assert (
|
||||
headers.Accept.parse(value).match(
|
||||
bool(headers.MediaType._parse(value).match(
|
||||
other,
|
||||
allow_type_wildcard=allow_type,
|
||||
allow_subtype_wildcard=allow_subtype,
|
||||
)
|
||||
))
|
||||
is outcome
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("foo/bar", "foo/*", "*/*"))
|
||||
def test_value_in_accept(value):
|
||||
acceptable = headers.parse_accept(value)
|
||||
assert "foo/bar" in acceptable
|
||||
assert "foo/*" in acceptable
|
||||
assert "*/*" in acceptable
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("foo/bar", "foo/*"))
|
||||
def test_value_not_in_accept(value):
|
||||
acceptable = headers.parse_accept(value)
|
||||
assert "no/match" not in acceptable
|
||||
assert "no/*" not in acceptable
|
||||
assert "*/*" not in acceptable
|
||||
assert "*/bar" not in acceptable
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -355,16 +299,22 @@ def test_value_not_in_accept(value):
|
||||
(
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", # noqa: E501
|
||||
[
|
||||
"text/html",
|
||||
"application/xhtml+xml",
|
||||
"image/avif",
|
||||
"image/webp",
|
||||
"application/xml;q=0.9",
|
||||
"*/*;q=0.8",
|
||||
("text/html", 1.0),
|
||||
("application/xhtml+xml", 1.0),
|
||||
("image/avif", 1.0),
|
||||
("image/webp", 1.0),
|
||||
("application/xml", 0.9),
|
||||
("*/*", 0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_browser_headers(header, expected):
|
||||
mimes = [e[0] for e in expected]
|
||||
qs = [e[1] for e in expected]
|
||||
request = Request(b"/", {"accept": header}, "1.1", "GET", None, None)
|
||||
assert request.accept == expected
|
||||
assert request.accept == mimes
|
||||
for a, m, q in zip(request.accept, mimes, qs):
|
||||
assert a == m
|
||||
assert a.str == m
|
||||
assert a.q == q
|
||||
|
||||
Reference in New Issue
Block a user