Signals Integration (#2160)
* Update some tests * Resolve #2122 route decorator returning tuple * Use rc sanic-routing version * Update unit tests to <:str> * Minimal working version with some signals implemented * Add more http signals * Update ASGI and change listeners to signals * Allow for dynamic ODE signals * Allow signals to be stacked * Begin tests * Prioritize match_info on keyword argument injection * WIP on tests * Compat with signals * Work through some test coverage * Passing tests * Post linting * Setup proper resets * coverage reporting * Fixes from vltr comments * clear delayed tasks * Fix bad test * rm pycache
This commit is contained in:
246
sanic/app.py
246
sanic/app.py
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
import re
|
||||
|
||||
from asyncio import (
|
||||
AbstractEventLoop,
|
||||
CancelledError,
|
||||
Protocol,
|
||||
ensure_future,
|
||||
@@ -72,19 +75,27 @@ from sanic.server import AsyncioServer, HttpProtocol
|
||||
from sanic.server import Signal as ServerSignal
|
||||
from sanic.server import serve, serve_multiple, serve_single
|
||||
from sanic.signals import Signal, SignalRouter
|
||||
from sanic.touchup import TouchUp, TouchUpMeta
|
||||
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
||||
|
||||
|
||||
class Sanic(BaseSanic):
|
||||
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||
"""
|
||||
The main application instance
|
||||
"""
|
||||
|
||||
__touchup__ = (
|
||||
"handle_request",
|
||||
"handle_exception",
|
||||
"_run_response_middleware",
|
||||
"_run_request_middleware",
|
||||
)
|
||||
__fake_slots__ = (
|
||||
"_asgi_app",
|
||||
"_app_registry",
|
||||
"_asgi_client",
|
||||
"_blueprint_order",
|
||||
"_delayed_tasks",
|
||||
"_future_routes",
|
||||
"_future_statics",
|
||||
"_future_middleware",
|
||||
@@ -155,6 +166,7 @@ class Sanic(BaseSanic):
|
||||
|
||||
self._asgi_client = None
|
||||
self._blueprint_order: List[Blueprint] = []
|
||||
self._delayed_tasks: List[str] = []
|
||||
self._test_client = None
|
||||
self._test_manager = None
|
||||
self.asgi = False
|
||||
@@ -192,6 +204,7 @@ class Sanic(BaseSanic):
|
||||
self.__class__.register_app(self)
|
||||
|
||||
self.router.ctx.app = self
|
||||
self.signal_router.ctx.app = self
|
||||
|
||||
if dumps:
|
||||
BaseHTTPResponse._dumps = dumps # type: ignore
|
||||
@@ -232,9 +245,12 @@ class Sanic(BaseSanic):
|
||||
loop = self.loop # Will raise SanicError if loop is not started
|
||||
self._loop_add_task(task, self, loop)
|
||||
except SanicException:
|
||||
self.listener("before_server_start")(
|
||||
partial(self._loop_add_task, task)
|
||||
)
|
||||
task_name = f"sanic.delayed_task.{hash(task)}"
|
||||
if not self._delayed_tasks:
|
||||
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
||||
|
||||
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
||||
self._delayed_tasks.append(task_name)
|
||||
|
||||
def register_listener(self, listener: Callable, event: str) -> Any:
|
||||
"""
|
||||
@@ -246,12 +262,20 @@ class Sanic(BaseSanic):
|
||||
"""
|
||||
|
||||
try:
|
||||
_event = ListenerEvent(event)
|
||||
except ValueError:
|
||||
valid = ", ".join(ListenerEvent.__members__.values())
|
||||
_event = ListenerEvent[event.upper()]
|
||||
except (ValueError, AttributeError):
|
||||
valid = ", ".join(
|
||||
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
|
||||
)
|
||||
raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}")
|
||||
|
||||
self.listeners[_event].append(listener)
|
||||
if "." in _event:
|
||||
self.signal(_event.value)(
|
||||
partial(self._listener, listener=listener)
|
||||
)
|
||||
else:
|
||||
self.listeners[_event.value].append(listener)
|
||||
|
||||
return listener
|
||||
|
||||
def register_middleware(self, middleware, attach_to: str = "request"):
|
||||
@@ -379,11 +403,17 @@ class Sanic(BaseSanic):
|
||||
*,
|
||||
condition: Optional[Dict[str, str]] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
fail_not_found: bool = True,
|
||||
inline: bool = False,
|
||||
reverse: bool = False,
|
||||
) -> Coroutine[Any, Any, Awaitable[Any]]:
|
||||
return self.signal_router.dispatch(
|
||||
event,
|
||||
context=context,
|
||||
condition=condition,
|
||||
inline=inline,
|
||||
reverse=reverse,
|
||||
fail_not_found=fail_not_found,
|
||||
)
|
||||
|
||||
async def event(
|
||||
@@ -659,7 +689,7 @@ class Sanic(BaseSanic):
|
||||
|
||||
async def handle_exception(
|
||||
self, request: Request, exception: BaseException
|
||||
):
|
||||
): # no cov
|
||||
"""
|
||||
A handler that catches specific exceptions and outputs a response.
|
||||
|
||||
@@ -669,6 +699,12 @@ class Sanic(BaseSanic):
|
||||
:type exception: BaseException
|
||||
:raises ServerError: response 500
|
||||
"""
|
||||
await self.dispatch(
|
||||
"http.lifecycle.exception",
|
||||
inline=True,
|
||||
context={"request": request, "exception": exception},
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
@@ -715,7 +751,7 @@ class Sanic(BaseSanic):
|
||||
f"Invalid response type {response!r} (need HTTPResponse)"
|
||||
)
|
||||
|
||||
async def handle_request(self, request: Request):
|
||||
async def handle_request(self, request: Request): # no cov
|
||||
"""Take a request from the HTTP Server and return a response object
|
||||
to be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
@@ -723,10 +759,22 @@ class Sanic(BaseSanic):
|
||||
:param request: HTTP Request object
|
||||
:return: Nothing
|
||||
"""
|
||||
await self.dispatch(
|
||||
"http.lifecycle.handle",
|
||||
inline=True,
|
||||
context={"request": request},
|
||||
)
|
||||
|
||||
# Define `response` var here to remove warnings about
|
||||
# allocation before assignment below.
|
||||
response = None
|
||||
try:
|
||||
|
||||
await self.dispatch(
|
||||
"http.routing.before",
|
||||
inline=True,
|
||||
context={"request": request},
|
||||
)
|
||||
# Fetch handler from router
|
||||
route, handler, kwargs = self.router.get(
|
||||
request.path,
|
||||
@@ -734,9 +782,20 @@ class Sanic(BaseSanic):
|
||||
request.headers.getone("host", None),
|
||||
)
|
||||
|
||||
request._match_info = kwargs
|
||||
request._match_info = {**kwargs}
|
||||
request.route = route
|
||||
|
||||
await self.dispatch(
|
||||
"http.routing.after",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"route": route,
|
||||
"kwargs": kwargs,
|
||||
"handler": handler,
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
request.stream
|
||||
and request.stream.request_body
|
||||
@@ -772,7 +831,7 @@ class Sanic(BaseSanic):
|
||||
)
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, **kwargs)
|
||||
response = handler(request, **request.match_info)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
@@ -783,6 +842,14 @@ class Sanic(BaseSanic):
|
||||
|
||||
# Make sure that response is finished / run StreamingHTTP callback
|
||||
if isinstance(response, BaseHTTPResponse):
|
||||
await self.dispatch(
|
||||
"http.lifecycle.response",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"response": response,
|
||||
},
|
||||
)
|
||||
await response.send(end_stream=True)
|
||||
else:
|
||||
if not hasattr(handler, "is_websocket"):
|
||||
@@ -1078,11 +1145,6 @@ class Sanic(BaseSanic):
|
||||
run_async=return_asyncio_server,
|
||||
)
|
||||
|
||||
# Trigger before_start events
|
||||
await self.trigger_events(
|
||||
server_settings.get("before_start", []),
|
||||
server_settings.get("loop"),
|
||||
)
|
||||
main_start = server_settings.pop("main_start", None)
|
||||
main_stop = server_settings.pop("main_stop", None)
|
||||
if main_start or main_stop:
|
||||
@@ -1095,17 +1157,9 @@ class Sanic(BaseSanic):
|
||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||
)
|
||||
|
||||
async def trigger_events(self, events, loop):
|
||||
"""Trigger events (functions or async)
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
await result
|
||||
|
||||
async def _run_request_middleware(self, request, request_name=None):
|
||||
async def _run_request_middleware(
|
||||
self, request, request_name=None
|
||||
): # no cov
|
||||
# The if improves speed. I don't know why
|
||||
named_middleware = self.named_request_middleware.get(
|
||||
request_name, deque()
|
||||
@@ -1118,25 +1172,67 @@ class Sanic(BaseSanic):
|
||||
request.request_middleware_started = True
|
||||
|
||||
for middleware in applicable_middleware:
|
||||
await self.dispatch(
|
||||
"http.middleware.before",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"response": None,
|
||||
},
|
||||
condition={"attach_to": "request"},
|
||||
)
|
||||
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
await self.dispatch(
|
||||
"http.middleware.after",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"response": None,
|
||||
},
|
||||
condition={"attach_to": "request"},
|
||||
)
|
||||
|
||||
if response:
|
||||
return response
|
||||
return None
|
||||
|
||||
async def _run_response_middleware(
|
||||
self, request, response, request_name=None
|
||||
):
|
||||
): # no cov
|
||||
named_middleware = self.named_response_middleware.get(
|
||||
request_name, deque()
|
||||
)
|
||||
applicable_middleware = self.response_middleware + named_middleware
|
||||
if applicable_middleware:
|
||||
for middleware in applicable_middleware:
|
||||
await self.dispatch(
|
||||
"http.middleware.before",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"response": response,
|
||||
},
|
||||
condition={"attach_to": "response"},
|
||||
)
|
||||
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
|
||||
await self.dispatch(
|
||||
"http.middleware.after",
|
||||
inline=True,
|
||||
context={
|
||||
"request": request,
|
||||
"response": _response if _response else response,
|
||||
},
|
||||
condition={"attach_to": "response"},
|
||||
)
|
||||
|
||||
if _response:
|
||||
response = _response
|
||||
if isinstance(response, BaseHTTPResponse):
|
||||
@@ -1162,10 +1258,6 @@ class Sanic(BaseSanic):
|
||||
):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
|
||||
self.listeners["before_server_start"] = [
|
||||
self.finalize
|
||||
] + self.listeners["before_server_start"]
|
||||
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
cert = ssl.get("cert") or ssl.get("certificate")
|
||||
@@ -1202,10 +1294,6 @@ class Sanic(BaseSanic):
|
||||
# Register start/stop events
|
||||
|
||||
for event_name, settings_name, reverse in (
|
||||
("before_server_start", "before_start", False),
|
||||
("after_server_start", "after_start", False),
|
||||
("before_server_stop", "before_stop", True),
|
||||
("after_server_stop", "after_stop", True),
|
||||
("main_process_start", "main_start", False),
|
||||
("main_process_stop", "main_stop", True),
|
||||
):
|
||||
@@ -1253,20 +1341,44 @@ class Sanic(BaseSanic):
|
||||
return ".".join(parts)
|
||||
|
||||
@classmethod
|
||||
def _loop_add_task(cls, task, app, loop):
|
||||
def _prep_task(cls, task, app, loop):
|
||||
if callable(task):
|
||||
try:
|
||||
loop.create_task(task(app))
|
||||
task = task(app)
|
||||
except TypeError:
|
||||
loop.create_task(task())
|
||||
else:
|
||||
loop.create_task(task)
|
||||
task = task()
|
||||
|
||||
return task
|
||||
|
||||
@classmethod
|
||||
def _loop_add_task(cls, task, app, loop):
|
||||
prepped = cls._prep_task(task, app, loop)
|
||||
loop.create_task(prepped)
|
||||
|
||||
@classmethod
|
||||
def _cancel_websocket_tasks(cls, app, loop):
|
||||
for task in app.websocket_tasks:
|
||||
task.cancel()
|
||||
|
||||
@staticmethod
|
||||
async def dispatch_delayed_tasks(app, loop):
|
||||
for name in app._delayed_tasks:
|
||||
await app.dispatch(name, context={"app": app, "loop": loop})
|
||||
app._delayed_tasks.clear()
|
||||
|
||||
@staticmethod
|
||||
async def run_delayed_task(app, loop, task):
|
||||
prepped = app._prep_task(task, app, loop)
|
||||
await prepped
|
||||
|
||||
@staticmethod
|
||||
async def _listener(
|
||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||
):
|
||||
maybe_coro = listener(app, loop)
|
||||
if maybe_coro and isawaitable(maybe_coro):
|
||||
await maybe_coro
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# ASGI
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -1340,15 +1452,51 @@ class Sanic(BaseSanic):
|
||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Static methods
|
||||
# Lifecycle
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
@staticmethod
|
||||
async def finalize(app, _):
|
||||
def finalize(self):
|
||||
try:
|
||||
app.router.finalize()
|
||||
if app.signal_router.routes:
|
||||
app.signal_router.finalize() # noqa
|
||||
self.router.finalize()
|
||||
except FinalizationError as e:
|
||||
if not Sanic.test_mode:
|
||||
raise e # noqa
|
||||
raise e
|
||||
|
||||
def signalize(self):
|
||||
try:
|
||||
self.signal_router.finalize()
|
||||
except FinalizationError as e:
|
||||
if not Sanic.test_mode:
|
||||
raise e
|
||||
|
||||
async def _startup(self):
|
||||
self.signalize()
|
||||
self.finalize()
|
||||
TouchUp.run(self)
|
||||
|
||||
async def _server_event(
|
||||
self,
|
||||
concern: str,
|
||||
action: str,
|
||||
loop: Optional[AbstractEventLoop] = None,
|
||||
) -> None:
|
||||
event = f"server.{concern}.{action}"
|
||||
if action not in ("before", "after") or concern not in (
|
||||
"init",
|
||||
"shutdown",
|
||||
):
|
||||
raise SanicException(f"Invalid server event: {event}")
|
||||
logger.debug(f"Triggering server events: {event}")
|
||||
reverse = concern == "shutdown"
|
||||
if loop is None:
|
||||
loop = self.loop
|
||||
await self.dispatch(
|
||||
event,
|
||||
fail_not_found=False,
|
||||
reverse=reverse,
|
||||
inline=True,
|
||||
context={
|
||||
"app": self,
|
||||
"loop": loop,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user