From dadf76ce727441f2f42c5bfa5d5f7a046e70d7b9 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 27 Jan 2021 10:25:05 +0200 Subject: [PATCH] Move logic into mixins --- sanic/app.py | 128 ++++++------------------------------- sanic/blueprint_group.py | 5 +- sanic/blueprints.py | 119 +++++++++------------------------- sanic/mixins/base.py | 19 ++++++ sanic/mixins/middleware.py | 41 ++++++++++++ sanic/mixins/routes.py | 113 ++++++++++++++++++++++---------- sanic/models/futures.py | 18 ++++-- sanic/static.py | 46 ++++++------- 8 files changed, 224 insertions(+), 265 deletions(-) create mode 100644 sanic/mixins/base.py create mode 100644 sanic/mixins/middleware.py diff --git a/sanic/app.py b/sanic/app.py index 303b2f75..3075549c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -30,8 +30,10 @@ 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.middleware import MiddlewareMixin from sanic.mixins.routes import RouteMixin -from sanic.models.futures import FutureRoute +from sanic.models.futures import FutureMiddleware, FutureRoute, FutureStatic from sanic.request import Request from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.router import Router @@ -47,7 +49,7 @@ from sanic.views import CompositionView from sanic.websocket import ConnectionClosed, WebSocketProtocol -class Sanic(RouteMixin): +class Sanic(BaseMixin, RouteMixin, MiddlewareMixin): _app_registry: Dict[str, "Sanic"] = {} test_mode = False @@ -65,7 +67,6 @@ class Sanic(RouteMixin): ) -> None: super().__init__() - # Get name from previous stack frame if name is None: raise SanicException( "Sanic instance cannot be unnamed. " @@ -169,44 +170,8 @@ class Sanic(RouteMixin): def _apply_route(self, route: FutureRoute) -> Route: return self.router.add(**route._asdict()) - def add_websocket_route( - self, - handler, - uri, - host=None, - strict_slashes=None, - subprotocols=None, - version=None, - name=None, - ): - """ - A helper method to register a function as a websocket route. - - :param handler: a callable function or instance of a class - that can handle the websocket request - :param host: Host IP or FQDN details - :param uri: URL path that will be mapped to the websocket - handler - handler - :param strict_slashes: If the API endpoint needs to terminate - with a "/" or not - :param subprotocols: Subprotocols to be used with websocket - handshake - :param name: A unique name assigned to the URL so that it can - be used with :func:`url_for` - :return: Objected decorated by :func:`websocket` - """ - if strict_slashes is None: - strict_slashes = self.strict_slashes - - return self.websocket( - uri, - host=host, - strict_slashes=strict_slashes, - subprotocols=subprotocols, - version=version, - name=name, - )(handler) + def _apply_static(self, static: FutureStatic) -> Route: + return static_register(self, static) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. @@ -281,77 +246,20 @@ class Sanic(RouteMixin): self.named_response_middleware[_rn].appendleft(middleware) # Decorator - def middleware(self, middleware_or_request): - """ - Decorate and register middleware to be called before a request. - Can either be called as *@app.middleware* or - *@app.middleware('request')* - - :param: middleware_or_request: Optional parameter to use for - identifying which type of middleware is being registered. - """ - # Detect which way this was called, @middleware or @middleware('AT') - if callable(middleware_or_request): - return self.register_middleware(middleware_or_request) - - else: - return partial( - self.register_middleware, attach_to=middleware_or_request - ) - - # Static Files - def static( + def _apply_middleware( self, - uri, - file_or_directory, - pattern=r"/?.+", - use_modified_since=True, - use_content_range=False, - stream_large_files=False, - name="static", - host=None, - strict_slashes=None, - content_type=None, + middleware: FutureMiddleware, + route_names: Optional[List[str]] = None, ): - """ - Register a root to serve files from. The input can either be a - file or a directory. This method will enable an easy and simple way - to setup the :class:`Route` necessary to serve the static files. - - :param uri: URL path to be used for serving static content - :param file_or_directory: Path for the Static file/directory with - static files - :param pattern: Regex Pattern identifying the valid static files - :param use_modified_since: If true, send file modified time, and return - not modified if the browser's matches the server's - :param use_content_range: If true, process header for range requests - and sends the file part that is requested - :param stream_large_files: If true, use the - :func:`StreamingHTTPResponse.file_stream` handler rather - than the :func:`HTTPResponse.file` handler to send the file. - If this is an integer, this represents the threshold size to - switch to :func:`StreamingHTTPResponse.file_stream` - :param name: user defined name used for url_for - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param content_type: user defined content type for header - :return: routes registered on the router - :rtype: List[sanic.router.Route] - """ - return static_register( - self, - uri, - file_or_directory, - pattern, - use_modified_since, - use_content_range, - stream_large_files, - name, - host, - strict_slashes, - content_type, - ) + print(f"{middleware=}") + if route_names: + return self.register_named_middleware( + middleware.middleware, route_names, middleware.attach_to + ) + else: + return self.register_middleware( + middleware.middleware, middleware.attach_to + ) def blueprint(self, blueprint, **options): """Register a blueprint on the application. diff --git a/sanic/blueprint_group.py b/sanic/blueprint_group.py index e6e0ebbb..544f6aa8 100644 --- a/sanic/blueprint_group.py +++ b/sanic/blueprint_group.py @@ -112,10 +112,13 @@ class BlueprintGroup(MutableSequence): :param kwargs: Optional Keyword arg to use with Middleware :return: Partial function to apply the middleware """ - kwargs["bp_group"] = True def register_middleware_for_blueprints(fn): for blueprint in self.blueprints: blueprint.middleware(fn, *args, **kwargs) + if args and callable(args[0]): + fn = args[0] + args = list(args)[1:] + return register_middleware_for_blueprints(fn) return register_middleware_for_blueprints diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 591c96f3..3d0dc825 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -2,6 +2,8 @@ from collections import defaultdict, namedtuple from sanic.blueprint_group import BlueprintGroup from sanic.constants import HTTP_METHODS +from sanic.mixins.base import BaseMixin +from sanic.mixins.middleware import MiddlewareMixin from sanic.mixins.routes import RouteMixin from sanic.models.futures import ( FutureException, @@ -13,7 +15,7 @@ from sanic.models.futures import ( from sanic.views import CompositionView -class Blueprint(RouteMixin): +class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin): def __init__( self, name, @@ -34,8 +36,6 @@ class Blueprint(RouteMixin): :param strict_slashes: Enforce the API urls are requested with a training */* """ - super().__init__() - self.name = name self.url_prefix = url_prefix self.host = host @@ -53,6 +53,14 @@ class Blueprint(RouteMixin): kwargs["apply"] = False return super().route(*args, **kwargs) + def static(self, *args, **kwargs): + kwargs["apply"] = False + return super().static(*args, **kwargs) + + def middleware(self, *args, **kwargs): + kwargs["apply"] = False + return super().middleware(*args, **kwargs) + @staticmethod def group(*blueprints, url_prefix=""): """ @@ -118,51 +126,26 @@ class Blueprint(RouteMixin): future.ignore_body, ) - _route = app._apply_route(apply_route) + route = app._apply_route(apply_route) + routes.append(route) - # TODO: - # for future in self.websocket_routes: - # # attach the blueprint name to the handler so that it can be - # # prefixed properly in the router - # future.handler.__blueprintname__ = self.name - # # Prepend the blueprint URI prefix if available - # uri = url_prefix + future.uri if url_prefix else future.uri - # _routes, _ = app.websocket( - # uri=uri, - # host=future.host or self.host, - # strict_slashes=future.strict_slashes, - # name=future.name, - # )(future.handler) - # if _routes: - # routes += _routes + # Static Files + for future in self._future_statics: + # Prepend the blueprint URI prefix if available + uri = url_prefix + future.uri if url_prefix else future.uri + apply_route = FutureStatic(uri, *future[1:]) + route = app._apply_static(apply_route) + routes.append(route) - # # Static Files - # for future in self.statics: - # # Prepend the blueprint URI prefix if available - # uri = url_prefix + future.uri if url_prefix else future.uri - # _routes = app.static( - # uri, future.file_or_directory, *future.args, **future.kwargs - # ) - # if _routes: - # routes += _routes + route_names = [route.name for route in routes if route] - # route_names = [route.name for route in routes if route] + # Middleware + for future in self._future_middleware: + app._apply_middleware(future, route_names) - # # Middleware - # for future in self.middlewares: - # if future.args or future.kwargs: - # app.register_named_middleware( - # future.middleware, - # route_names, - # *future.args, - # **future.kwargs, - # ) - # else: - # app.register_named_middleware(future.middleware, route_names) - - # # Exceptions - # for future in self.exceptions: - # app.exception(*future.args, **future.kwargs)(future.handler) + # Exceptions + for future in self.exceptions: + app.exception(*future.args, **future.kwargs)(future.handler) # Event listeners for event, listeners in self.listeners.items(): @@ -181,35 +164,6 @@ class Blueprint(RouteMixin): return decorator - def middleware(self, *args, **kwargs): - """ - Create a blueprint middleware from a decorated function. - - :param args: Positional arguments to be used while invoking the - middleware - :param kwargs: optional keyword args that can be used with the - middleware. - """ - - def register_middleware(_middleware): - future_middleware = FutureMiddleware(_middleware, args, kwargs) - self.middlewares.append(future_middleware) - return _middleware - - # Detect which way this was called, @middleware or @middleware('AT') - if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): - middleware = args[0] - args = [] - return register_middleware(middleware) - else: - if kwargs.get("bp_group") and callable(args[0]): - middleware = args[0] - args = args[1:] - kwargs.pop("bp_group") - return register_middleware(middleware) - else: - return register_middleware - def exception(self, *args, **kwargs): """ This method enables the process of creating a global exception @@ -230,20 +184,5 @@ class Blueprint(RouteMixin): return decorator - def static(self, uri, file_or_directory, *args, **kwargs): - """Create a blueprint static route from a decorated function. - - :param uri: endpoint at which the route will be accessible. - :param file_or_directory: Static asset. - """ - name = kwargs.pop("name", "static") - if not name.startswith(self.name + "."): - name = f"{self.name}.{name}" - kwargs.update(name=name) - - strict_slashes = kwargs.get("strict_slashes") - if strict_slashes is None and self.strict_slashes is not None: - kwargs.update(strict_slashes=self.strict_slashes) - - static = FutureStatic(uri, file_or_directory, args, kwargs) - self.statics.append(static) + def _generate_name(self, handler, name: str) -> str: + return f"{self.name}.{name or handler.__name__}" diff --git a/sanic/mixins/base.py b/sanic/mixins/base.py new file mode 100644 index 00000000..eb55edc5 --- /dev/null +++ b/sanic/mixins/base.py @@ -0,0 +1,19 @@ +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): + ... diff --git a/sanic/mixins/middleware.py b/sanic/mixins/middleware.py new file mode 100644 index 00000000..58bda3be --- /dev/null +++ b/sanic/mixins/middleware.py @@ -0,0 +1,41 @@ +from functools import partial +from typing import Set + +from sanic.models.futures import FutureMiddleware + + +class MiddlewareMixin: + def __init__(self, *args, **kwargs) -> None: + self._future_middleware: Set[FutureMiddleware] = set() + + def _apply_middleware(self, middleware: FutureMiddleware): + raise NotImplementedError + + def middleware( + self, middleware_or_request, attach_to="request", apply=True + ): + """ + Decorate and register middleware to be called before a request. + Can either be called as *@app.middleware* or + *@app.middleware('request')* + + :param: middleware_or_request: Optional parameter to use for + identifying which type of middleware is being registered. + """ + + def register_middleware(_middleware, attach_to="request"): + future_middleware = FutureMiddleware(_middleware, attach_to) + self._future_middleware.add(future_middleware) + if apply: + self._apply_middleware(future_middleware) + return _middleware + + # Detect which way this was called, @middleware or @middleware('AT') + if callable(middleware_or_request): + return register_middleware( + middleware_or_request, attach_to=attach_to + ) + else: + return partial( + register_middleware, attach_to=middleware_or_request + ) diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index d97a3d6b..ef5a4bdd 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -1,25 +1,31 @@ from functools import partial from inspect import signature -from typing import List, Set +from pathlib import PurePath +from typing import List, Set, Union import websockets from sanic_routing.route import Route from sanic.constants import HTTP_METHODS -from sanic.models.futures import FutureRoute +from sanic.models.futures import FutureRoute, FutureStatic from sanic.views import CompositionView class RouteMixin: - def __init__(self) -> None: - self._future_routes: Set[Route] = set() - self._future_websocket_routes: Set[Route] = set() + def __init__(self, *args, **kwargs) -> None: + self._future_routes: Set[FutureRoute] = set() + self._future_statics: Set[FutureStatic] = set() + self.name = "" + self.strict_slashes = False def _apply_route(self, route: FutureRoute) -> Route: raise NotImplementedError - def _route( + def _apply_static(self, static: FutureStatic) -> Route: + raise NotImplementedError + + def route( self, uri, methods=frozenset({"GET"}), @@ -133,30 +139,6 @@ class RouteMixin: return decorator - def route( - self, - uri, - methods=frozenset({"GET"}), - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ignore_body=False, - apply=True, - ): - return self._route( - uri=uri, - methods=methods, - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ignore_body=ignore_body, - apply=apply, - ) - def add_route( self, handler, @@ -435,7 +417,7 @@ class RouteMixin: :param version: Blueprint Version :param name: Unique name to identify the Websocket Route """ - return self._route( + return self.route( uri=uri, host=host, methods=None, @@ -474,11 +456,8 @@ class RouteMixin: be used with :func:`url_for` :return: Objected decorated by :func:`websocket` """ - if strict_slashes is None: - strict_slashes = self.strict_slashes - return self.websocket( - uri, + uri=uri, host=host, strict_slashes=strict_slashes, subprotocols=subprotocols, @@ -486,5 +465,69 @@ class RouteMixin: name=name, )(handler) + def static( + self, + uri, + file_or_directory: Union[str, bytes, PurePath], + pattern=r"/?.+", + use_modified_since=True, + use_content_range=False, + stream_large_files=False, + name="static", + host=None, + strict_slashes=None, + content_type=None, + apply=True, + ): + """ + Register a root to serve files from. The input can either be a + file or a directory. This method will enable an easy and simple way + to setup the :class:`Route` necessary to serve the static files. + + :param uri: URL path to be used for serving static content + :param file_or_directory: Path for the Static file/directory with + static files + :param pattern: Regex Pattern identifying the valid static files + :param use_modified_since: If true, send file modified time, and return + not modified if the browser's matches the server's + :param use_content_range: If true, process header for range requests + and sends the file part that is requested + :param stream_large_files: If true, use the + :func:`StreamingHTTPResponse.file_stream` handler rather + than the :func:`HTTPResponse.file` handler to send the file. + If this is an integer, this represents the threshold size to + switch to :func:`StreamingHTTPResponse.file_stream` + :param name: user defined name used for url_for + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param content_type: user defined content type for header + :return: routes registered on the router + :rtype: List[sanic.router.Route] + """ + + if not name.startswith(self.name + "."): + name = f"{self.name}.{name}" + + if strict_slashes is None and self.strict_slashes is not None: + strict_slashes = self.strict_slashes + + static = FutureStatic( + uri, + file_or_directory, + pattern, + use_modified_since, + use_content_range, + stream_large_files, + name, + host, + strict_slashes, + content_type, + ) + self._future_statics.add(static) + + if apply: + self._apply_static(static) + def _generate_name(self, handler, name: str) -> str: return name or handler.__name__ diff --git a/sanic/models/futures.py b/sanic/models/futures.py index 94ff9ddf..aeca2c48 100644 --- a/sanic/models/futures.py +++ b/sanic/models/futures.py @@ -18,10 +18,20 @@ FutureRoute = namedtuple( FutureListener = namedtuple( "FutureListener", ["handler", "uri", "methods", "host"] ) -FutureMiddleware = namedtuple( - "FutureMiddleware", ["middleware", "args", "kwargs"] -) +FutureMiddleware = namedtuple("FutureMiddleware", ["middleware", "attach_to"]) FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"]) FutureStatic = namedtuple( - "FutureStatic", ["uri", "file_or_directory", "args", "kwargs"] + "FutureStatic", + [ + "uri", + "file_or_directory", + "pattern", + "use_modified_since", + "use_content_range", + "stream_large_files", + "name", + "host", + "strict_slashes", + "content_type", + ], ) diff --git a/sanic/static.py b/sanic/static.py index f0943a7d..768a57ce 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -16,6 +16,7 @@ from sanic.exceptions import ( ) from sanic.handlers import ContentRangeHandler from sanic.log import error_logger +from sanic.models.futures import FutureStatic from sanic.response import HTTPResponse, file, file_stream @@ -112,16 +113,7 @@ async def _static_request_handler( def register( app, - uri: str, - file_or_directory: Union[str, bytes, PurePath], - pattern, - use_modified_since, - use_content_range, - stream_large_files, - name: str = "static", - host=None, - strict_slashes=None, - content_type=None, + static: FutureStatic, ): # TODO: Though sanic is not a file server, I feel like we should at least # make a good effort here. Modified-since is nice, but we could @@ -152,38 +144,42 @@ def register( :rtype: List[sanic.router.Route] """ - if isinstance(file_or_directory, bytes): - file_or_directory = file_or_directory.decode("utf-8") - elif isinstance(file_or_directory, PurePath): - file_or_directory = str(file_or_directory) - elif not isinstance(file_or_directory, str): + if isinstance(static.file_or_directory, bytes): + file_or_directory = static.file_or_directory.decode("utf-8") + elif isinstance(static.file_or_directory, PurePath): + file_or_directory = str(static.file_or_directory) + elif not isinstance(static.file_or_directory, str): raise ValueError("Invalid file path string.") + else: + file_or_directory = static.file_or_directory + uri = static.uri + name = static.name # If we're not trying to match a file directly, # serve from the folder if not path.isfile(file_or_directory): - uri += "" + uri += "" # special prefix for static files - if not name.startswith("_static_"): - name = f"_static_{name}" + if not static.name.startswith("_static_"): + name = f"_static_{static.name}" _handler = wraps(_static_request_handler)( partial( _static_request_handler, file_or_directory, - use_modified_since, - use_content_range, - stream_large_files, - content_type=content_type, + static.use_modified_since, + static.use_content_range, + static.stream_large_files, + content_type=static.content_type, ) ) _routes, _ = app.route( - uri, + uri=uri, methods=["GET", "HEAD"], name=name, - host=host, - strict_slashes=strict_slashes, + host=static.host, + strict_slashes=static.strict_slashes, )(_handler) return _routes