Finish moving some more logic to mixins

This commit is contained in:
Adam Hopkins 2021-01-27 15:57:21 +02:00
parent dadf76ce72
commit e9459792a4
9 changed files with 157 additions and 88 deletions

View File

@ -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,24 +196,21 @@ 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:
for exception in handler.exceptions:
if isinstance(exception, (tuple, list)):
for e in exception:
self.error_handler.add(e, handler)
self.error_handler.add(e, handler.handler)
else:
self.error_handler.add(exception, handler)
self.error_handler.add(exception, handler.handler)
return handler
return response
def register_middleware(self, middleware, attach_to="request"):
"""
Register an application level middleware that will be attached

View File

@ -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__}"

View File

@ -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

55
sanic/mixins/listeners.py Normal file
View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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",
[

View File

@ -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

View File

@ -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