Smarter auto fallback (#2162)

* Smarter auto fallback

* remove config from blueprints

* Add tests for error formatting

* Add check for proper format

* Fix some tests

* Add some tests

* docstring

* Add accept matching

* Add some more tests on matching

* Fix contains bug, earlier return on MediaType eq

* Add matching flags for wildcards

* Add mathing controls to accept

* Cleanup dev cruft

* Add cleanup and resolve OSError relating to test implementation

* Fix test

* Fix some typos
This commit is contained in:
Adam Hopkins
2021-09-29 23:53:49 +03:00
committed by GitHub
parent b5f2bd9b0e
commit cf1d2148ac
16 changed files with 562 additions and 125 deletions

View File

@@ -1,17 +1,20 @@
from ast import NodeVisitor, Return, parse
from functools import partial, wraps
from inspect import signature
from inspect import getsource, signature
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from re import sub
from textwrap import dedent
from time import gmtime, strftime
from typing import Callable, Iterable, List, Optional, Set, Tuple, Union
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
@@ -61,6 +64,7 @@ class RouteMixin:
unquote: bool = False,
static: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Decorate a function to be registered as a route
@@ -103,6 +107,7 @@ class RouteMixin:
nonlocal websocket
nonlocal static
nonlocal version_prefix
nonlocal error_format
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
@@ -128,6 +133,9 @@ class RouteMixin:
# subprotocol is unordered, keep it unordered
subprotocols = frozenset(subprotocols)
if not error_format or error_format == "auto":
error_format = self._determine_error_format(handler)
route = FutureRoute(
handler,
uri,
@@ -143,6 +151,7 @@ class RouteMixin:
unquote,
static,
version_prefix,
error_format,
)
self._future_routes.add(route)
@@ -186,6 +195,7 @@ class RouteMixin:
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
@@ -236,6 +246,7 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)(handler)
return handler
@@ -249,6 +260,7 @@ class RouteMixin:
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
@@ -272,6 +284,7 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
def post(
@@ -283,6 +296,7 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
@@ -306,6 +320,7 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
def put(
@@ -317,6 +332,7 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
@@ -340,6 +356,7 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
def head(
@@ -351,6 +368,7 @@ class RouteMixin:
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -382,6 +400,7 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
def options(
@@ -393,6 +412,7 @@ class RouteMixin:
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -424,6 +444,7 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
def patch(
@@ -435,6 +456,7 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -468,6 +490,7 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
def delete(
@@ -479,6 +502,7 @@ class RouteMixin:
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -502,6 +526,7 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
def websocket(
@@ -514,6 +539,7 @@ class RouteMixin:
name: Optional[str] = None,
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
):
"""
Decorate a function to be registered as a websocket route
@@ -540,6 +566,7 @@ class RouteMixin:
subprotocols=subprotocols,
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
)
def add_websocket_route(
@@ -552,6 +579,7 @@ class RouteMixin:
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
):
"""
A helper method to register a function as a websocket route.
@@ -580,6 +608,7 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)(handler)
def static(
@@ -789,8 +818,8 @@ class RouteMixin:
)
except Exception:
error_logger.exception(
f"Exception in static request handler:\
path={file_or_directory}, "
f"Exception in static request handler: "
f"path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise
@@ -888,3 +917,43 @@ class RouteMixin:
)(_handler)
return route
def _determine_error_format(self, handler) -> str:
if not isinstance(handler, CompositionView):
try:
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1:
return next(iter(http_response_types))
except OSError:
...
return "auto"
def _get_response_types(self, node):
types = set()
class HttpResponseVisitor(NodeVisitor):
def visit_Return(self, node: Return) -> Any:
nonlocal types
try:
checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore
checks += [
k.value
for k in node.value.keywords # type: ignore
if k.arg == "content_type"
]
for check in checks:
if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check])
except AttributeError:
...
HttpResponseVisitor().visit(node)
return types