Merge in latest from sanic-routing branch
This commit is contained in:
339
sanic/app.py
339
sanic/app.py
@@ -25,27 +25,24 @@ from typing import (
|
||||
)
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
|
||||
from sanic_routing.route import Route
|
||||
from sanic_routing.exceptions import FinalizationError # type: ignore
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic import reloader_helpers
|
||||
from sanic.asgi import ASGIApp
|
||||
from sanic.base import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.config import BASE_LOGO, Config
|
||||
from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
NotFound,
|
||||
SanicException,
|
||||
ServerError,
|
||||
URLBuildError,
|
||||
)
|
||||
from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
|
||||
from sanic.mixins.base import BaseMixin
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerEvent, ListenerMixin
|
||||
from sanic.mixins.middleware import MiddlewareMixin
|
||||
from sanic.mixins.routes import RouteMixin
|
||||
from sanic.mixins.listeners import ListenerEvent
|
||||
from sanic.models.futures import (
|
||||
FutureException,
|
||||
FutureListener,
|
||||
@@ -68,9 +65,7 @@ from sanic.static import register as static_register
|
||||
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
||||
|
||||
|
||||
class Sanic(
|
||||
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
|
||||
):
|
||||
class Sanic(BaseSanic):
|
||||
"""
|
||||
The main application instance
|
||||
"""
|
||||
@@ -103,9 +98,7 @@ class Sanic(
|
||||
|
||||
self.name = name
|
||||
self.asgi = False
|
||||
self.router = router or Router(
|
||||
exception=NotFound, method_handler_exception=NotFound
|
||||
)
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config(load_env=load_env)
|
||||
@@ -124,6 +117,9 @@ class Sanic(
|
||||
self.websocket_tasks: Set[Future] = set()
|
||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||
# self.named_request_middleware: Dict[str, MiddlewareType] = {}
|
||||
# self.named_response_middleware: Dict[str, MiddlewareType] = {}
|
||||
self._test_manager = None
|
||||
self._test_client = None
|
||||
self._asgi_client = None
|
||||
# Register alternative method names
|
||||
@@ -135,6 +131,8 @@ class Sanic(
|
||||
if self.config.REGISTER:
|
||||
self.__class__.register_app(self)
|
||||
|
||||
self.router.ctx.app = self
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
"""
|
||||
@@ -175,10 +173,6 @@ class Sanic(
|
||||
partial(self._loop_add_task, task)
|
||||
)
|
||||
|
||||
# Decorator
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
return self.register_listener(listener.listener, listener.event)
|
||||
|
||||
def register_listener(self, listener: Callable, event: str) -> Any:
|
||||
"""
|
||||
Register the listener for a given event.
|
||||
@@ -197,42 +191,6 @@ class Sanic(
|
||||
self.listeners[_event].append(listener)
|
||||
return listener
|
||||
|
||||
def _apply_route(self, route: FutureRoute) -> Route:
|
||||
return self.router.add(**route._asdict())
|
||||
|
||||
def _apply_static(self, static: FutureStatic) -> Route:
|
||||
return static_register(self, static)
|
||||
|
||||
def enable_websocket(self, enable: bool = True):
|
||||
"""
|
||||
Enable or disable the support for websocket.
|
||||
|
||||
Websocket is enabled automatically if websocket routes are
|
||||
added to the application.
|
||||
"""
|
||||
if not self.websocket_enabled:
|
||||
# if the server is stopped, we want to cancel any ongoing
|
||||
# websocket tasks, to allow the server to exit promptly
|
||||
self.listener("before_server_stop")(self._cancel_websocket_tasks)
|
||||
|
||||
self.websocket_enabled = enable
|
||||
|
||||
# Decorator
|
||||
def _apply_exception_handler(self, handler: FutureException):
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
:param exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
for exception in handler.exceptions:
|
||||
if isinstance(exception, (tuple, list)):
|
||||
for e in exception:
|
||||
self.error_handler.add(e, handler.handler)
|
||||
else:
|
||||
self.error_handler.add(exception, handler.handler)
|
||||
return handler
|
||||
|
||||
def register_middleware(self, middleware, attach_to: str = "request"):
|
||||
"""
|
||||
Register an application level middleware that will be attached
|
||||
@@ -288,13 +246,49 @@ class Sanic(
|
||||
if middleware not in self.named_response_middleware[_rn]:
|
||||
self.named_response_middleware[_rn].appendleft(middleware)
|
||||
|
||||
# Decorator
|
||||
def _apply_exception_handler(self, handler: FutureException):
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
:param exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
for exception in handler.exceptions:
|
||||
if isinstance(exception, (tuple, list)):
|
||||
for e in exception:
|
||||
self.error_handler.add(e, handler.handler)
|
||||
else:
|
||||
self.error_handler.add(exception, handler.handler)
|
||||
return handler
|
||||
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
return self.register_listener(listener.listener, listener.event)
|
||||
|
||||
def _apply_route(self, route: FutureRoute) -> Route:
|
||||
params = route._asdict()
|
||||
websocket = params.pop("websocket", False)
|
||||
subprotocols = params.pop("subprotocols", None)
|
||||
|
||||
if websocket:
|
||||
self.enable_websocket()
|
||||
websocket_handler = partial(
|
||||
self._websocket_handler,
|
||||
route.handler,
|
||||
subprotocols=subprotocols,
|
||||
)
|
||||
websocket_handler.__name__ = route.handler.__name__ # type: ignore
|
||||
websocket_handler.is_websocket = True # type: ignore
|
||||
params["handler"] = websocket_handler
|
||||
return self.router.add(**params)
|
||||
|
||||
def _apply_static(self, static: FutureStatic) -> Route:
|
||||
return static_register(self, static)
|
||||
|
||||
def _apply_middleware(
|
||||
self,
|
||||
middleware: FutureMiddleware,
|
||||
route_names: Optional[List[str]] = None,
|
||||
):
|
||||
print(f"{middleware=}")
|
||||
if route_names:
|
||||
return self.register_named_middleware(
|
||||
middleware.middleware, route_names, middleware.attach_to
|
||||
@@ -304,6 +298,19 @@ class Sanic(
|
||||
middleware.middleware, middleware.attach_to
|
||||
)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
|
||||
Websocket is enabled automatically if websocket routes are
|
||||
added to the application.
|
||||
"""
|
||||
if not self.websocket_enabled:
|
||||
# if the server is stopped, we want to cancel any ongoing
|
||||
# websocket tasks, to allow the server to exit promptly
|
||||
self.listener("before_server_stop")(self._cancel_websocket_tasks)
|
||||
|
||||
self.websocket_enabled = enable
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
@@ -323,6 +330,12 @@ class Sanic(
|
||||
else:
|
||||
self.blueprints[blueprint.name] = blueprint
|
||||
self._blueprint_order.append(blueprint)
|
||||
|
||||
if (
|
||||
self.strict_slashes is not None
|
||||
and blueprint.strict_slashes is None
|
||||
):
|
||||
blueprint.strict_slashes = self.strict_slashes
|
||||
blueprint.register(self, options)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs):
|
||||
@@ -351,30 +364,28 @@ class Sanic(
|
||||
# find the route by the supplied view name
|
||||
kw: Dict[str, str] = {}
|
||||
# special static files url_for
|
||||
if view_name == "static":
|
||||
kw.update(name=kwargs.pop("name", "static"))
|
||||
elif view_name.endswith(".static"): # blueprint.static
|
||||
kwargs.pop("name", None)
|
||||
|
||||
if "." not in view_name:
|
||||
view_name = f"{self.name}.{view_name}"
|
||||
|
||||
if view_name.endswith(".static"):
|
||||
name = kwargs.pop("name", None)
|
||||
if name:
|
||||
view_name = view_name.replace("static", name)
|
||||
kw.update(name=view_name)
|
||||
|
||||
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
||||
if not (uri and route):
|
||||
route = self.router.find_route_by_view_name(view_name, **kw)
|
||||
if not route:
|
||||
raise URLBuildError(
|
||||
f"Endpoint with name `{view_name}` was not found"
|
||||
)
|
||||
|
||||
# If the route has host defined, split that off
|
||||
# TODO: Retain netloc and path separately in Route objects
|
||||
host = uri.find("/")
|
||||
if host > 0:
|
||||
host, uri = uri[:host], uri[host:]
|
||||
else:
|
||||
host = None
|
||||
uri = route.path
|
||||
|
||||
if view_name == "static" or view_name.endswith(".static"):
|
||||
filename = kwargs.pop("filename", None)
|
||||
if getattr(route.ctx, "static", None):
|
||||
filename = kwargs.pop("filename", "")
|
||||
# it's static folder
|
||||
if "<file_uri:" in uri:
|
||||
if "file_uri" in uri:
|
||||
folder_ = uri.split("<file_uri:", 1)[0]
|
||||
if folder_.endswith("/"):
|
||||
folder_ = folder_[:-1]
|
||||
@@ -382,22 +393,36 @@ class Sanic(
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
|
||||
uri = f"{folder_}/{filename}"
|
||||
kwargs["file_uri"] = filename
|
||||
|
||||
if uri != "/" and uri.endswith("/"):
|
||||
uri = uri[:-1]
|
||||
|
||||
out = uri
|
||||
if not uri.startswith("/"):
|
||||
uri = f"/{uri}"
|
||||
|
||||
# find all the parameters we will need to build in the URL
|
||||
matched_params = re.findall(self.router.parameter_pattern, uri)
|
||||
out = uri
|
||||
|
||||
# _method is only a placeholder now, don't know how to support it
|
||||
kwargs.pop("_method", None)
|
||||
anchor = kwargs.pop("_anchor", "")
|
||||
# _external need SERVER_NAME in config or pass _server arg
|
||||
external = kwargs.pop("_external", False)
|
||||
host = kwargs.pop("_host", None)
|
||||
external = kwargs.pop("_external", False) or bool(host)
|
||||
scheme = kwargs.pop("_scheme", "")
|
||||
if route.ctx.hosts and external:
|
||||
if not host and len(route.ctx.hosts) > 1:
|
||||
raise ValueError(
|
||||
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
|
||||
)
|
||||
elif host and host not in route.ctx.hosts:
|
||||
raise ValueError(
|
||||
f"Requested host ({host}) is not available for this "
|
||||
f"route: {route.ctx.hosts}"
|
||||
)
|
||||
elif not host:
|
||||
host = list(route.ctx.hosts)[0]
|
||||
|
||||
if scheme and not external:
|
||||
raise ValueError("When specifying _scheme, _external must be True")
|
||||
|
||||
@@ -415,44 +440,44 @@ class Sanic(
|
||||
if "://" in netloc[:8]:
|
||||
netloc = netloc.split("://", 1)[-1]
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(match)
|
||||
# find all the parameters we will need to build in the URL
|
||||
# matched_params = re.findall(self.router.parameter_pattern, uri)
|
||||
route.finalize()
|
||||
for param_info in route.params.values():
|
||||
# name, _type, pattern = self.router.parse_parameter_string(match)
|
||||
# we only want to match against each individual parameter
|
||||
specific_pattern = f"^{pattern}$"
|
||||
supplied_param = None
|
||||
|
||||
if name in kwargs:
|
||||
supplied_param = kwargs.get(name)
|
||||
del kwargs[name]
|
||||
else:
|
||||
try:
|
||||
supplied_param = str(kwargs.pop(param_info.name))
|
||||
except KeyError:
|
||||
raise URLBuildError(
|
||||
f"Required parameter `{name}` was not passed to url_for"
|
||||
f"Required parameter `{param_info.name}` was not "
|
||||
"passed to url_for"
|
||||
)
|
||||
|
||||
supplied_param = str(supplied_param)
|
||||
# determine if the parameter supplied by the caller passes the test
|
||||
# in the URL
|
||||
passes_pattern = re.match(specific_pattern, supplied_param)
|
||||
|
||||
if not passes_pattern:
|
||||
if _type != str:
|
||||
type_name = _type.__name__
|
||||
|
||||
msg = (
|
||||
f'Value "{supplied_param}" '
|
||||
f"for parameter `{name}` does not "
|
||||
f"match pattern for type `{type_name}`: {pattern}"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f'Value "{supplied_param}" for parameter `{name}` '
|
||||
f"does not satisfy pattern {pattern}"
|
||||
)
|
||||
raise URLBuildError(msg)
|
||||
# determine if the parameter supplied by the caller
|
||||
# passes the test in the URL
|
||||
if param_info.pattern:
|
||||
passes_pattern = param_info.pattern.match(supplied_param)
|
||||
if not passes_pattern:
|
||||
if param_info.cast != str:
|
||||
msg = (
|
||||
f'Value "{supplied_param}" '
|
||||
f"for parameter `{param_info.name}` does "
|
||||
"not match pattern for type "
|
||||
f"`{param_info.cast.__name__}`: "
|
||||
f"{param_info.pattern.pattern}"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f'Value "{supplied_param}" for parameter '
|
||||
f"`{param_info.name}` does not satisfy "
|
||||
f"pattern {param_info.pattern.pattern}"
|
||||
)
|
||||
raise URLBuildError(msg)
|
||||
|
||||
# replace the parameter in the URL with the supplied value
|
||||
replacement_regex = f"(<{name}.*?>)"
|
||||
|
||||
replacement_regex = f"(<{param_info.name}.*?>)"
|
||||
out = re.sub(replacement_regex, supplied_param, out)
|
||||
|
||||
# parse the remainder of the keyword arguments into a querystring
|
||||
@@ -545,14 +570,13 @@ class Sanic(
|
||||
# Fetch handler from router
|
||||
(
|
||||
handler,
|
||||
args,
|
||||
kwargs,
|
||||
uri,
|
||||
name,
|
||||
endpoint,
|
||||
ignore_body,
|
||||
) = self.router.get(request)
|
||||
request.name = name
|
||||
request._match_info = kwargs
|
||||
|
||||
if (
|
||||
request.stream
|
||||
@@ -578,7 +602,7 @@ class Sanic(
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.uri_template = uri
|
||||
request.uri_template = f"/{uri}"
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
(
|
||||
@@ -587,10 +611,10 @@ class Sanic(
|
||||
)
|
||||
)
|
||||
|
||||
request.endpoint = endpoint
|
||||
request.endpoint = request.name
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
response = handler(request, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
@@ -615,26 +639,60 @@ class Sanic(
|
||||
except CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
await self.handle_exception(request, e)
|
||||
|
||||
async def _websocket_handler(
|
||||
self, handler, request, *args, subprotocols=None, **kwargs
|
||||
):
|
||||
request.app = self
|
||||
if not getattr(handler, "__blueprintname__", False):
|
||||
request.endpoint = handler.__name__
|
||||
else:
|
||||
request.endpoint = (
|
||||
getattr(handler, "__blueprintname__", "") + handler.__name__
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
if self.asgi:
|
||||
ws = request.transport.get_websocket_connection()
|
||||
else:
|
||||
protocol = request.transport.get_protocol()
|
||||
protocol.app = self
|
||||
|
||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.add(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
pass
|
||||
finally:
|
||||
self.websocket_tasks.remove(fut)
|
||||
await ws.close()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Testing
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def test_client(self):
|
||||
def test_client(self): # noqa
|
||||
if self._test_client:
|
||||
return self._test_client
|
||||
elif self._test_manager:
|
||||
return self._test_manager.test_client
|
||||
from sanic_testing.testing import SanicTestClient # type: ignore
|
||||
|
||||
self._test_client = SanicTestClient(self)
|
||||
return self._test_client
|
||||
|
||||
@property
|
||||
def asgi_client(self):
|
||||
def asgi_client(self): # noqa
|
||||
"""
|
||||
A testing client that uses ASGI to reach into the application to
|
||||
execute hanlers.
|
||||
@@ -644,6 +702,8 @@ class Sanic(
|
||||
"""
|
||||
if self._asgi_client:
|
||||
return self._asgi_client
|
||||
elif self._test_manager:
|
||||
return self._test_manager.asgi_client
|
||||
from sanic_testing.testing import SanicASGITestClient # type: ignore
|
||||
|
||||
self._asgi_client = SanicASGITestClient(self)
|
||||
@@ -915,7 +975,11 @@ class Sanic(
|
||||
):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
|
||||
self.router.finalize()
|
||||
try:
|
||||
self.router.finalize()
|
||||
except FinalizationError as e:
|
||||
if not Sanic.test_mode:
|
||||
raise e
|
||||
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
@@ -950,9 +1014,7 @@ class Sanic(
|
||||
"backlog": backlog,
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, reverse in (
|
||||
("before_server_start", "before_start", False),
|
||||
@@ -1014,40 +1076,6 @@ class Sanic(
|
||||
for task in app.websocket_tasks:
|
||||
task.cancel()
|
||||
|
||||
async def _websocket_handler(
|
||||
self, handler, request, *args, subprotocols=None, **kwargs
|
||||
):
|
||||
request.app = self
|
||||
if not getattr(handler, "__blueprintname__", False):
|
||||
request.endpoint = handler.__name__
|
||||
else:
|
||||
request.endpoint = (
|
||||
getattr(handler, "__blueprintname__", "") + handler.__name__
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
if self.asgi:
|
||||
ws = request.transport.get_websocket_connection()
|
||||
else:
|
||||
protocol = request.transport.get_protocol()
|
||||
protocol.app = self
|
||||
|
||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.add(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
pass
|
||||
finally:
|
||||
self.websocket_tasks.remove(fut)
|
||||
await ws.close()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# ASGI
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -1057,9 +1085,10 @@ class Sanic(
|
||||
To be ASGI compliant, our instance must be a callable that accepts
|
||||
three arguments: scope, receive, send. See the ASGI reference for more
|
||||
details: https://asgi.readthedocs.io/en/latest
|
||||
/"""
|
||||
"""
|
||||
self.asgi = True
|
||||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
asgi_app = self._asgi_app
|
||||
await asgi_app()
|
||||
|
||||
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||
|
||||
@@ -131,6 +131,7 @@ class Lifespan:
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
startup event.
|
||||
"""
|
||||
self.asgi_app.sanic_app.router.finalize()
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_start", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||
|
||||
36
sanic/base.py
Normal file
36
sanic/base.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerMixin
|
||||
from sanic.mixins.middleware import MiddlewareMixin
|
||||
from sanic.mixins.routes import RouteMixin
|
||||
|
||||
|
||||
class Base(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
init = attrs.get("__init__")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
nonlocal init
|
||||
nonlocal name
|
||||
|
||||
bases = [
|
||||
b for base in type(self).__bases__ for b in base.__bases__
|
||||
]
|
||||
|
||||
for base in bases:
|
||||
base.__init__(self, *args, **kwargs)
|
||||
|
||||
if init:
|
||||
init(self, *args, **kwargs)
|
||||
|
||||
attrs["__init__"] = __init__
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class BaseSanic(
|
||||
RouteMixin,
|
||||
MiddlewareMixin,
|
||||
ListenerMixin,
|
||||
ExceptionMixin,
|
||||
metaclass=Base,
|
||||
):
|
||||
...
|
||||
@@ -1,18 +1,12 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
from typing import Iterable, Optional
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
|
||||
from sanic.base import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.mixins.base import BaseMixin
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerMixin
|
||||
from sanic.mixins.middleware import MiddlewareMixin
|
||||
from sanic.mixins.routes import RouteMixin
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
|
||||
|
||||
class Blueprint(
|
||||
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
|
||||
):
|
||||
class Blueprint(BaseSanic):
|
||||
"""
|
||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||
URLs that perform a specific set of tasks which can be identified by
|
||||
@@ -122,20 +116,35 @@ class Blueprint(
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
|
||||
strict_slashes = (
|
||||
self.strict_slashes
|
||||
if future.strict_slashes is None
|
||||
and self.strict_slashes is not None
|
||||
else future.strict_slashes
|
||||
)
|
||||
name = app._generate_name(future.name)
|
||||
|
||||
apply_route = FutureRoute(
|
||||
future.handler,
|
||||
uri[1:] if uri.startswith("//") else uri,
|
||||
future.methods,
|
||||
future.host or self.host,
|
||||
future.strict_slashes,
|
||||
strict_slashes,
|
||||
future.stream,
|
||||
future.version or self.version,
|
||||
future.name,
|
||||
name,
|
||||
future.ignore_body,
|
||||
future.websocket,
|
||||
future.subprotocols,
|
||||
future.unquote,
|
||||
future.static,
|
||||
)
|
||||
|
||||
route = app._apply_route(apply_route)
|
||||
routes.append(route)
|
||||
operation = (
|
||||
routes.extend if isinstance(route, list) else routes.append
|
||||
)
|
||||
operation(route)
|
||||
|
||||
# Static Files
|
||||
for future in self._future_statics:
|
||||
@@ -148,8 +157,9 @@ class Blueprint(
|
||||
route_names = [route.name for route in routes if route]
|
||||
|
||||
# Middleware
|
||||
for future in self._future_middleware:
|
||||
app._apply_middleware(future, route_names)
|
||||
if route_names:
|
||||
for future in self._future_middleware:
|
||||
app._apply_middleware(future, route_names)
|
||||
|
||||
# Exceptions
|
||||
for future in self._future_exceptions:
|
||||
@@ -158,6 +168,3 @@ class Blueprint(
|
||||
# Event listeners
|
||||
for listener in self._future_listeners:
|
||||
app._apply_listener(listener)
|
||||
|
||||
def _generate_name(self, handler, name: str) -> str:
|
||||
return f"{self.name}.{name or handler.__name__}"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
class Base(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
init = attrs.get("__init__")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
nonlocal init
|
||||
for base in type(self).__bases__:
|
||||
if base.__name__ != "BaseMixin":
|
||||
base.__init__(self, *args, **kwargs)
|
||||
|
||||
if init:
|
||||
init(self, *args, **kwargs)
|
||||
|
||||
attrs["__init__"] = __init__
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class BaseMixin(metaclass=Base):
|
||||
...
|
||||
@@ -1,5 +1,3 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import Set
|
||||
|
||||
from sanic.models.futures import FutureException
|
||||
@@ -29,6 +27,9 @@ class ExceptionMixin:
|
||||
nonlocal apply
|
||||
nonlocal exceptions
|
||||
|
||||
if isinstance(exceptions[0], list):
|
||||
exceptions = tuple(*exceptions)
|
||||
|
||||
future_exception = FutureException(handler, exceptions)
|
||||
self._future_exceptions.add(future_exception)
|
||||
if apply:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Coroutine, Optional, Set, Union
|
||||
from typing import Any, Callable, Coroutine, List, Optional, Set, Union
|
||||
|
||||
from sanic.models.futures import FutureListener
|
||||
|
||||
@@ -17,7 +17,7 @@ class ListenerEvent(str, Enum):
|
||||
|
||||
class ListenerMixin:
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_listeners: Set[FutureListener] = set()
|
||||
self._future_listeners: List[FutureListener] = list()
|
||||
|
||||
def _apply_listener(self, listener: FutureListener):
|
||||
raise NotImplementedError
|
||||
@@ -51,7 +51,7 @@ class ListenerMixin:
|
||||
nonlocal apply
|
||||
|
||||
future_listener = FutureListener(listener, event)
|
||||
self._future_listeners.add(future_listener)
|
||||
self._future_listeners.append(future_listener)
|
||||
if apply:
|
||||
self._apply_listener(future_listener)
|
||||
return listener
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from functools import partial
|
||||
from typing import Set
|
||||
from typing import List
|
||||
|
||||
from sanic.models.futures import FutureMiddleware
|
||||
|
||||
|
||||
class MiddlewareMixin:
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_middleware: Set[FutureMiddleware] = set()
|
||||
self._future_middleware: List[FutureMiddleware] = list()
|
||||
|
||||
def _apply_middleware(self, middleware: FutureMiddleware):
|
||||
raise NotImplementedError
|
||||
@@ -30,7 +30,7 @@ class MiddlewareMixin:
|
||||
nonlocal apply
|
||||
|
||||
future_middleware = FutureMiddleware(middleware, attach_to)
|
||||
self._future_middleware.add(future_middleware)
|
||||
self._future_middleware.append(future_middleware)
|
||||
if apply:
|
||||
self._apply_middleware(future_middleware)
|
||||
return middleware
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from functools import partial
|
||||
from inspect import signature
|
||||
from pathlib import PurePath
|
||||
from typing import Iterable, List, Optional, Set, Union
|
||||
|
||||
from sanic_routing.route import Route
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
@@ -36,6 +35,8 @@ class RouteMixin:
|
||||
apply: bool = True,
|
||||
subprotocols: Optional[List[str]] = None,
|
||||
websocket: bool = False,
|
||||
unquote: bool = False,
|
||||
static: bool = False,
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a route
|
||||
@@ -52,9 +53,6 @@ class RouteMixin:
|
||||
:return: tuple of routes, decorated function
|
||||
"""
|
||||
|
||||
if websocket:
|
||||
self.enable_websocket()
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith("/"):
|
||||
@@ -63,6 +61,9 @@ class RouteMixin:
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
if not methods and not websocket:
|
||||
methods = frozenset({"GET"})
|
||||
|
||||
def decorator(handler):
|
||||
nonlocal uri
|
||||
nonlocal methods
|
||||
@@ -74,39 +75,43 @@ class RouteMixin:
|
||||
nonlocal ignore_body
|
||||
nonlocal subprotocols
|
||||
nonlocal websocket
|
||||
nonlocal static
|
||||
|
||||
if isinstance(handler, tuple):
|
||||
# if a handler fn is already wrapped in a route, the handler
|
||||
# variable will be a tuple of (existing routes, handler fn)
|
||||
_, handler = handler
|
||||
|
||||
if websocket:
|
||||
websocket_handler = partial(
|
||||
self._websocket_handler,
|
||||
handler,
|
||||
subprotocols=subprotocols,
|
||||
)
|
||||
websocket_handler.__name__ = (
|
||||
"websocket_handler_" + handler.__name__
|
||||
)
|
||||
websocket_handler.is_websocket = True
|
||||
handler = websocket_handler
|
||||
name = self._generate_name(name, handler)
|
||||
|
||||
# TODO:
|
||||
# - THink this thru.... do we want all routes namespaced?
|
||||
# -
|
||||
name = self._generate_name(handler, name)
|
||||
if isinstance(host, str):
|
||||
host = frozenset([host])
|
||||
elif host and not isinstance(host, frozenset):
|
||||
try:
|
||||
host = frozenset(host)
|
||||
except TypeError:
|
||||
raise ValueError(
|
||||
"Expected either string or Iterable of host strings, "
|
||||
"not %s" % host
|
||||
)
|
||||
|
||||
if isinstance(subprotocols, (list, tuple, set)):
|
||||
subprotocols = frozenset(subprotocols)
|
||||
|
||||
route = FutureRoute(
|
||||
handler,
|
||||
uri,
|
||||
frozenset(methods),
|
||||
None if websocket else frozenset([x.upper() for x in methods]),
|
||||
host,
|
||||
strict_slashes,
|
||||
stream,
|
||||
version,
|
||||
name,
|
||||
ignore_body,
|
||||
websocket,
|
||||
subprotocols,
|
||||
unquote,
|
||||
static,
|
||||
)
|
||||
|
||||
self._future_routes.add(route)
|
||||
@@ -441,6 +446,7 @@ class RouteMixin:
|
||||
subprotocols: Optional[List[str]] = None,
|
||||
version: Optional[int] = None,
|
||||
name: Optional[str] = None,
|
||||
apply: bool = True,
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a websocket route
|
||||
@@ -543,12 +549,16 @@ class RouteMixin:
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
|
||||
if not name.startswith(self.name + "."):
|
||||
name = f"{self.name}.{name}"
|
||||
name = self._generate_name(name)
|
||||
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
if not isinstance(file_or_directory, (str, bytes, PurePath)):
|
||||
raise ValueError(
|
||||
f"Static route must be a valid path, not {file_or_directory}"
|
||||
)
|
||||
|
||||
static = FutureStatic(
|
||||
uri,
|
||||
file_or_directory,
|
||||
@@ -566,5 +576,29 @@ class RouteMixin:
|
||||
if apply:
|
||||
self._apply_static(static)
|
||||
|
||||
def _generate_name(self, handler, name: str) -> str:
|
||||
return name or handler.__name__
|
||||
def _generate_name(self, *objects) -> str:
|
||||
name = None
|
||||
|
||||
for obj in objects:
|
||||
if obj:
|
||||
if isinstance(obj, str):
|
||||
name = obj
|
||||
break
|
||||
|
||||
try:
|
||||
name = obj.name
|
||||
except AttributeError:
|
||||
try:
|
||||
name = obj.__name__
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
if not name:
|
||||
raise Exception("...")
|
||||
|
||||
if not name.startswith(f"{self.name}."):
|
||||
name = f"{self.name}.{name}"
|
||||
|
||||
return name
|
||||
|
||||
@@ -13,6 +13,10 @@ FutureRoute = namedtuple(
|
||||
"version",
|
||||
"name",
|
||||
"ignore_body",
|
||||
"websocket",
|
||||
"subprotocols",
|
||||
"unquote",
|
||||
"static",
|
||||
],
|
||||
)
|
||||
FutureListener = namedtuple("FutureListener", ["listener", "event"])
|
||||
|
||||
@@ -87,6 +87,7 @@ class Request:
|
||||
"_port",
|
||||
"_remote_addr",
|
||||
"_socket",
|
||||
"_match_info",
|
||||
"app",
|
||||
"body",
|
||||
"conn_info",
|
||||
@@ -147,6 +148,7 @@ class Request:
|
||||
self.uri_template: Optional[str] = None
|
||||
self.request_middleware_started = False
|
||||
self._cookies: Dict[str, str] = {}
|
||||
self._match_info = {}
|
||||
self.stream: Optional[Http] = None
|
||||
self.endpoint: Optional[str] = None
|
||||
|
||||
@@ -455,7 +457,7 @@ class Request:
|
||||
"""
|
||||
:return: matched info after resolving route
|
||||
"""
|
||||
return self.app.router.get(self)[2]
|
||||
return self._match_info
|
||||
|
||||
# Transport properties (obtained from local interface only)
|
||||
|
||||
|
||||
175
sanic/router.py
175
sanic/router.py
@@ -1,14 +1,22 @@
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, Iterable, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from sanic_routing import BaseRouter
|
||||
from sanic_routing.route import Route
|
||||
from sanic_routing import BaseRouter # type: ignore
|
||||
from sanic_routing.exceptions import NoMethod # type: ignore
|
||||
from sanic_routing.exceptions import (
|
||||
NotFound as RoutingNotFound, # type: ignore
|
||||
)
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import MethodNotSupported, NotFound
|
||||
from sanic.handlers import RouteHandler
|
||||
from sanic.request import Request
|
||||
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
class Router(BaseRouter):
|
||||
"""
|
||||
The router implementation responsible for routing a :class:`Request` object
|
||||
@@ -18,18 +26,38 @@ class Router(BaseRouter):
|
||||
DEFAULT_METHOD = "GET"
|
||||
ALLOWED_METHODS = HTTP_METHODS
|
||||
|
||||
@lru_cache
|
||||
def get(
|
||||
self, request: Request
|
||||
) -> Tuple[
|
||||
RouteHandler,
|
||||
Tuple[Any, ...],
|
||||
Dict[str, Any],
|
||||
str,
|
||||
str,
|
||||
Optional[str],
|
||||
bool,
|
||||
]:
|
||||
# Putting the lru_cache on Router.get() performs better for the benchmarsk
|
||||
# at tests/benchmark/test_route_resolution_benchmark.py
|
||||
# However, overall application performance is significantly improved
|
||||
# with the lru_cache on this method.
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(
|
||||
self, path, method, host
|
||||
) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool,]:
|
||||
try:
|
||||
route, handler, params = self.resolve(
|
||||
path=path,
|
||||
method=method,
|
||||
extra={"host": host},
|
||||
)
|
||||
except RoutingNotFound as e:
|
||||
raise NotFound("Requested URL {} not found".format(e.path))
|
||||
except NoMethod as e:
|
||||
raise MethodNotSupported(
|
||||
"Method {} not allowed for URL {}".format(method, path),
|
||||
method=method,
|
||||
allowed_methods=e.allowed_methods,
|
||||
)
|
||||
|
||||
return (
|
||||
handler,
|
||||
params,
|
||||
route.path,
|
||||
route.name,
|
||||
route.ctx.ignore_body,
|
||||
)
|
||||
|
||||
def get(self, request: Request):
|
||||
"""
|
||||
Retrieve a `Route` object containg the details about how to handle
|
||||
a response for a given request
|
||||
@@ -41,23 +69,8 @@ class Router(BaseRouter):
|
||||
:rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str,
|
||||
Optional[str], bool, ]
|
||||
"""
|
||||
route, handler, params = self.resolve(
|
||||
path=request.path,
|
||||
method=request.method,
|
||||
)
|
||||
|
||||
# TODO: Implement response
|
||||
# - args,
|
||||
# - endpoint,
|
||||
|
||||
return (
|
||||
handler,
|
||||
(),
|
||||
params,
|
||||
route.path,
|
||||
route.name,
|
||||
None,
|
||||
route.ctx.ignore_body,
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("host")
|
||||
)
|
||||
|
||||
def add(
|
||||
@@ -65,13 +78,15 @@ class Router(BaseRouter):
|
||||
uri: str,
|
||||
methods: Iterable[str],
|
||||
handler: RouteHandler,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, Iterable[str]]] = None,
|
||||
strict_slashes: bool = False,
|
||||
stream: bool = False,
|
||||
ignore_body: bool = False,
|
||||
version: Union[str, float, int] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> Route:
|
||||
unquote: bool = False,
|
||||
static: bool = False,
|
||||
) -> Union[Route, List[Route]]:
|
||||
"""
|
||||
Add a handler to the router
|
||||
|
||||
@@ -99,19 +114,93 @@ class Router(BaseRouter):
|
||||
:return: the route object
|
||||
:rtype: Route
|
||||
"""
|
||||
# TODO: Implement
|
||||
# - host
|
||||
# - strict_slashes
|
||||
# - ignore_body
|
||||
# - stream
|
||||
if version is not None:
|
||||
version = str(version).strip("/").lstrip("v")
|
||||
uri = "/".join([f"/v{version}", uri.lstrip("/")])
|
||||
|
||||
route = super().add(
|
||||
path=uri, handler=handler, methods=methods, name=name
|
||||
params = dict(
|
||||
path=uri,
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
name=name,
|
||||
strict=strict_slashes,
|
||||
unquote=unquote,
|
||||
)
|
||||
route.ctx.ignore_body = ignore_body
|
||||
route.ctx.stream = stream
|
||||
|
||||
if isinstance(host, str):
|
||||
hosts = [host]
|
||||
else:
|
||||
hosts = host or [None] # type: ignore
|
||||
|
||||
routes = []
|
||||
|
||||
for host in hosts:
|
||||
if host:
|
||||
params.update({"requirements": {"host": host}})
|
||||
|
||||
route = super().add(**params)
|
||||
route.ctx.ignore_body = ignore_body
|
||||
route.ctx.stream = stream
|
||||
route.ctx.hosts = hosts
|
||||
route.ctx.static = static
|
||||
|
||||
routes.append(route)
|
||||
|
||||
if len(routes) == 1:
|
||||
return routes[0]
|
||||
return routes
|
||||
|
||||
def is_stream_handler(self, request) -> bool:
|
||||
"""
|
||||
Handler for request is stream or not.
|
||||
|
||||
:param request: Request object
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, MethodNotSupported):
|
||||
return False
|
||||
if hasattr(handler, "view_class") and hasattr(
|
||||
handler.view_class, request.method.lower()
|
||||
):
|
||||
handler = getattr(handler.view_class, request.method.lower())
|
||||
return hasattr(handler, "is_stream")
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""
|
||||
Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:param kwargs: additional params, usually for static files
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
if not view_name:
|
||||
return None
|
||||
|
||||
route = self.name_index.get(view_name)
|
||||
if not route:
|
||||
full_name = self.ctx.app._generate_name(view_name)
|
||||
route = self.name_index.get(full_name)
|
||||
|
||||
if not route:
|
||||
return None
|
||||
|
||||
return route
|
||||
|
||||
@property
|
||||
def routes_all(self):
|
||||
return self.routes
|
||||
|
||||
@property
|
||||
def routes_static(self):
|
||||
return self.static_routes
|
||||
|
||||
@property
|
||||
def routes_dynamic(self):
|
||||
return self.dynamic_routes
|
||||
|
||||
@property
|
||||
def routes_regex(self):
|
||||
return self.regex_routes
|
||||
|
||||
@@ -157,11 +157,11 @@ def register(
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += "<file_uri:" + static.pattern + ">"
|
||||
uri += "/<file_uri>"
|
||||
|
||||
# special prefix for static files
|
||||
if not static.name.startswith("_static_"):
|
||||
name = f"_static_{static.name}"
|
||||
# if not static.name.startswith("_static_"):
|
||||
# name = f"_static_{static.name}"
|
||||
|
||||
_handler = wraps(_static_request_handler)(
|
||||
partial(
|
||||
@@ -174,11 +174,13 @@ def register(
|
||||
)
|
||||
)
|
||||
|
||||
_routes, _ = app.route(
|
||||
route, _ = app.route(
|
||||
uri=uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
host=static.host,
|
||||
strict_slashes=static.strict_slashes,
|
||||
static=True,
|
||||
)(_handler)
|
||||
return _routes
|
||||
|
||||
return route
|
||||
|
||||
Reference in New Issue
Block a user