diff --git a/sanic/app.py b/sanic/app.py index 3075549c..8896acfd 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -23,6 +23,7 @@ from sanic.blueprints import Blueprint from sanic.config import BASE_LOGO, Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ( + InvalidUsage, NotFound, SanicException, ServerError, @@ -31,9 +32,17 @@ from sanic.exceptions import ( 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.models.futures import FutureMiddleware, FutureRoute, FutureStatic +from sanic.models.futures import ( + FutureException, + FutureListener, + FutureMiddleware, + FutureRoute, + FutureStatic, +) from sanic.request import Request from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.router import Router @@ -49,7 +58,9 @@ from sanic.views import CompositionView from sanic.websocket import ConnectionClosed, WebSocketProtocol -class Sanic(BaseMixin, RouteMixin, MiddlewareMixin): +class Sanic( + BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin +): _app_registry: Dict[str, "Sanic"] = {} test_mode = False @@ -144,17 +155,8 @@ class Sanic(BaseMixin, RouteMixin, MiddlewareMixin): ) # Decorator - def listener(self, event): - """Create a listener from a decorated function. - - :param event: event to listen to - """ - - def decorator(listener): - self.listeners[event].append(listener) - return listener - - return decorator + def _apply_listener(self, listener: FutureListener): + return self.register_listener(listener.listener, listener.event) def register_listener(self, listener, event): """ @@ -165,7 +167,14 @@ class Sanic(BaseMixin, RouteMixin, MiddlewareMixin): :return: listener """ - return self.listener(event)(listener) + try: + _event = ListenerEvent(event) + except ValueError: + valid = ", ".join(ListenerEvent.__members__.values()) + raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}") + + self.listeners[_event].append(listener) + return listener def _apply_route(self, route: FutureRoute) -> Route: return self.router.add(**route._asdict()) @@ -187,23 +196,20 @@ class Sanic(BaseMixin, RouteMixin, MiddlewareMixin): self.websocket_enabled = enable # Decorator - def exception(self, *exceptions): + def _apply_exception_handler(self, handler: FutureException): """Decorate a function to be registered as a handler for exceptions :param exceptions: exceptions :return: decorated function """ - def response(handler): - for exception in exceptions: - if isinstance(exception, (tuple, list)): - for e in exception: - self.error_handler.add(e, handler) - else: - self.error_handler.add(exception, handler) - return handler - - return response + 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="request"): """ diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 3d0dc825..5d6f9aa3 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -1,21 +1,17 @@ -from collections import defaultdict, namedtuple +from collections import defaultdict from sanic.blueprint_group import BlueprintGroup -from sanic.constants import HTTP_METHODS 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 ( - FutureException, - FutureListener, - FutureMiddleware, - FutureRoute, - FutureStatic, -) -from sanic.views import CompositionView +from sanic.models.futures import FutureRoute, FutureStatic -class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin): +class Blueprint( + BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin +): def __init__( self, name, @@ -61,6 +57,14 @@ class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin): kwargs["apply"] = False return super().middleware(*args, **kwargs) + def listener(self, *args, **kwargs): + kwargs["apply"] = False + return super().listener(*args, **kwargs) + + def exception(self, *args, **kwargs): + kwargs["apply"] = False + return super().exception(*args, **kwargs) + @staticmethod def group(*blueprints, url_prefix=""): """ @@ -103,9 +107,6 @@ class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin): routes = [] - # TODO: - # - Add BP name to handler name for all routes - # Routes for future in self._future_routes: # attach the blueprint name to the handler so that it can be @@ -144,45 +145,12 @@ class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin): app._apply_middleware(future, route_names) # Exceptions - for future in self.exceptions: - app.exception(*future.args, **future.kwargs)(future.handler) + for future in self._future_exceptions: + app._apply_exception_handler(future) # Event listeners - for event, listeners in self.listeners.items(): - for listener in listeners: - app.listener(event)(listener) - - def listener(self, event): - """Create a listener from a decorated function. - - :param event: Event to listen to. - """ - - def decorator(listener): - self.listeners[event].append(listener) - return listener - - return decorator - - def exception(self, *args, **kwargs): - """ - This method enables the process of creating a global exception - handler for the current blueprint under question. - - :param args: List of Python exceptions to be caught by the handler - :param kwargs: Additional optional arguments to be passed to the - exception handler - - :return a decorated method to handle global exceptions for any - route registered under this blueprint. - """ - - def decorator(handler): - exception = FutureException(handler, args, kwargs) - self.exceptions.append(exception) - return handler - - return decorator + 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__}" diff --git a/sanic/mixins/exceptions.py b/sanic/mixins/exceptions.py new file mode 100644 index 00000000..5792d68e --- /dev/null +++ b/sanic/mixins/exceptions.py @@ -0,0 +1,38 @@ +from enum import Enum, auto +from functools import partial +from typing import Set + +from sanic.models.futures import FutureException + + +class ExceptionMixin: + def __init__(self, *args, **kwargs) -> None: + self._future_exceptions: Set[FutureException] = set() + + def _apply_exception_handler(self, handler: FutureException): + raise NotImplementedError + + def exception(self, *exceptions, apply=True): + """ + This method enables the process of creating a global exception + handler for the current blueprint under question. + + :param args: List of Python exceptions to be caught by the handler + :param kwargs: Additional optional arguments to be passed to the + exception handler + + :return a decorated method to handle global exceptions for any + route registered under this blueprint. + """ + + def decorator(handler): + nonlocal apply + nonlocal exceptions + + future_exception = FutureException(handler, exceptions) + self._future_exceptions.add(future_exception) + if apply: + self._apply_exception_handler(future_exception) + return handler + + return decorator diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py new file mode 100644 index 00000000..6c27bc1d --- /dev/null +++ b/sanic/mixins/listeners.py @@ -0,0 +1,55 @@ +from enum import Enum, auto +from functools import partial +from typing import Set + +from sanic.models.futures import FutureListener + + +class ListenerEvent(str, Enum): + def _generate_next_value_(name: str, *args) -> str: # type: ignore + return name.lower() + + BEFORE_SERVER_START = auto() + AFTER_SERVER_START = auto() + BEFORE_SERVER_STOP = auto() + AFTER_SERVER_STOP = auto() + + +class ListenerMixin: + def __init__(self, *args, **kwargs) -> None: + self._future_listeners: Set[FutureListener] = set() + + def _apply_listener(self, listener: FutureListener): + raise NotImplementedError + + def listener(self, listener_or_event, event_or_none=None, apply=True): + """Create a listener from a decorated function. + + :param event: Event to listen to. + """ + + def register_listener(listener, event): + nonlocal apply + + future_listener = FutureListener(listener, event) + self._future_listeners.add(future_listener) + if apply: + self._apply_listener(future_listener) + return listener + + if callable(listener_or_event): + return register_listener(listener_or_event, event_or_none) + else: + return partial(register_listener, event=listener_or_event) + + def before_server_start(self, listener): + return self.listener(listener, "before_server_start") + + def after_server_start(self, listener): + return self.listener(listener, "after_server_start") + + def before_server_stop(self, listener): + return self.listener(listener, "before_server_stop") + + def after_server_stop(self, listener): + return self.listener(listener, "after_server_stop") diff --git a/sanic/mixins/middleware.py b/sanic/mixins/middleware.py index 58bda3be..f05c02b5 100644 --- a/sanic/mixins/middleware.py +++ b/sanic/mixins/middleware.py @@ -23,12 +23,14 @@ class MiddlewareMixin: identifying which type of middleware is being registered. """ - def register_middleware(_middleware, attach_to="request"): - future_middleware = FutureMiddleware(_middleware, attach_to) + def register_middleware(middleware, attach_to="request"): + nonlocal apply + + future_middleware = FutureMiddleware(middleware, attach_to) self._future_middleware.add(future_middleware) if apply: self._apply_middleware(future_middleware) - return _middleware + return middleware # Detect which way this was called, @middleware or @middleware('AT') if callable(middleware_or_request): @@ -39,3 +41,9 @@ class MiddlewareMixin: return partial( register_middleware, attach_to=middleware_or_request ) + + def on_request(self, middleware): + return self.middleware(middleware, "request") + + def on_response(self, middleware): + return self.middleware(middleware, "response") diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index ef5a4bdd..49505512 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -1,9 +1,7 @@ from functools import partial from inspect import signature from pathlib import PurePath -from typing import List, Set, Union - -import websockets +from typing import Set, Union from sanic_routing.route import Route diff --git a/sanic/models/futures.py b/sanic/models/futures.py index aeca2c48..bc68a9b3 100644 --- a/sanic/models/futures.py +++ b/sanic/models/futures.py @@ -15,11 +15,9 @@ FutureRoute = namedtuple( "ignore_body", ], ) -FutureListener = namedtuple( - "FutureListener", ["handler", "uri", "methods", "host"] -) +FutureListener = namedtuple("FutureListener", ["listener", "event"]) FutureMiddleware = namedtuple("FutureMiddleware", ["middleware", "attach_to"]) -FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"]) +FutureException = namedtuple("FutureException", ["handler", "exceptions"]) FutureStatic = namedtuple( "FutureStatic", [ diff --git a/sanic/router.py b/sanic/router.py index 9217243a..9ca59ab0 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -4,7 +4,6 @@ from sanic_routing import BaseRouter from sanic_routing.route import Route from sanic.constants import HTTP_METHODS -from sanic.log import logger from sanic.request import Request diff --git a/sanic/static.py b/sanic/static.py index 768a57ce..52db9c1c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -4,7 +4,6 @@ from os import path from pathlib import PurePath from re import sub from time import gmtime, strftime -from typing import Union from urllib.parse import unquote from sanic.compat import stat_async