Move logic into mixins

This commit is contained in:
Adam Hopkins 2021-01-27 10:25:05 +02:00
parent 33d7f4da6b
commit dadf76ce72
8 changed files with 224 additions and 265 deletions

View File

@ -30,8 +30,10 @@ from sanic.exceptions import (
) )
from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger 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.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.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse
from sanic.router import Router from sanic.router import Router
@ -47,7 +49,7 @@ from sanic.views import CompositionView
from sanic.websocket import ConnectionClosed, WebSocketProtocol from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic(RouteMixin): class Sanic(BaseMixin, RouteMixin, MiddlewareMixin):
_app_registry: Dict[str, "Sanic"] = {} _app_registry: Dict[str, "Sanic"] = {}
test_mode = False test_mode = False
@ -65,7 +67,6 @@ class Sanic(RouteMixin):
) -> None: ) -> None:
super().__init__() super().__init__()
# Get name from previous stack frame
if name is None: if name is None:
raise SanicException( raise SanicException(
"Sanic instance cannot be unnamed. " "Sanic instance cannot be unnamed. "
@ -169,44 +170,8 @@ class Sanic(RouteMixin):
def _apply_route(self, route: FutureRoute) -> Route: def _apply_route(self, route: FutureRoute) -> Route:
return self.router.add(**route._asdict()) return self.router.add(**route._asdict())
def add_websocket_route( def _apply_static(self, static: FutureStatic) -> Route:
self, return static_register(self, static)
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 enable_websocket(self, enable=True): def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket. """Enable or disable the support for websocket.
@ -281,76 +246,19 @@ class Sanic(RouteMixin):
self.named_response_middleware[_rn].appendleft(middleware) self.named_response_middleware[_rn].appendleft(middleware)
# Decorator # Decorator
def middleware(self, middleware_or_request): def _apply_middleware(
"""
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(
self, self,
uri, middleware: FutureMiddleware,
file_or_directory, route_names: Optional[List[str]] = None,
pattern=r"/?.+",
use_modified_since=True,
use_content_range=False,
stream_large_files=False,
name="static",
host=None,
strict_slashes=None,
content_type=None,
): ):
""" print(f"{middleware=}")
Register a root to serve files from. The input can either be a if route_names:
file or a directory. This method will enable an easy and simple way return self.register_named_middleware(
to setup the :class:`Route` necessary to serve the static files. middleware.middleware, route_names, middleware.attach_to
)
:param uri: URL path to be used for serving static content else:
:param file_or_directory: Path for the Static file/directory with return self.register_middleware(
static files middleware.middleware, middleware.attach_to
: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,
) )
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):

View File

@ -112,10 +112,13 @@ class BlueprintGroup(MutableSequence):
:param kwargs: Optional Keyword arg to use with Middleware :param kwargs: Optional Keyword arg to use with Middleware
:return: Partial function to apply the middleware :return: Partial function to apply the middleware
""" """
kwargs["bp_group"] = True
def register_middleware_for_blueprints(fn): def register_middleware_for_blueprints(fn):
for blueprint in self.blueprints: for blueprint in self.blueprints:
blueprint.middleware(fn, *args, **kwargs) 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 return register_middleware_for_blueprints

View File

@ -2,6 +2,8 @@ from collections import defaultdict, namedtuple
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.constants import HTTP_METHODS 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.mixins.routes import RouteMixin
from sanic.models.futures import ( from sanic.models.futures import (
FutureException, FutureException,
@ -13,7 +15,7 @@ from sanic.models.futures import (
from sanic.views import CompositionView from sanic.views import CompositionView
class Blueprint(RouteMixin): class Blueprint(BaseMixin, RouteMixin, MiddlewareMixin):
def __init__( def __init__(
self, self,
name, name,
@ -34,8 +36,6 @@ class Blueprint(RouteMixin):
:param strict_slashes: Enforce the API urls are requested with a :param strict_slashes: Enforce the API urls are requested with a
training */* training */*
""" """
super().__init__()
self.name = name self.name = name
self.url_prefix = url_prefix self.url_prefix = url_prefix
self.host = host self.host = host
@ -53,6 +53,14 @@ class Blueprint(RouteMixin):
kwargs["apply"] = False kwargs["apply"] = False
return super().route(*args, **kwargs) 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 @staticmethod
def group(*blueprints, url_prefix=""): def group(*blueprints, url_prefix=""):
""" """
@ -118,51 +126,26 @@ class Blueprint(RouteMixin):
future.ignore_body, future.ignore_body,
) )
_route = app._apply_route(apply_route) route = app._apply_route(apply_route)
routes.append(route)
# TODO: # Static Files
# for future in self.websocket_routes: for future in self._future_statics:
# # attach the blueprint name to the handler so that it can be # Prepend the blueprint URI prefix if available
# # prefixed properly in the router uri = url_prefix + future.uri if url_prefix else future.uri
# future.handler.__blueprintname__ = self.name apply_route = FutureStatic(uri, *future[1:])
# # Prepend the blueprint URI prefix if available route = app._apply_static(apply_route)
# uri = url_prefix + future.uri if url_prefix else future.uri routes.append(route)
# _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 route_names = [route.name for route in routes if route]
# 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] # Middleware
for future in self._future_middleware:
app._apply_middleware(future, route_names)
# # Middleware # Exceptions
# for future in self.middlewares: for future in self.exceptions:
# if future.args or future.kwargs: app.exception(*future.args, **future.kwargs)(future.handler)
# 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)
# Event listeners # Event listeners
for event, listeners in self.listeners.items(): for event, listeners in self.listeners.items():
@ -181,35 +164,6 @@ class Blueprint(RouteMixin):
return decorator 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): def exception(self, *args, **kwargs):
""" """
This method enables the process of creating a global exception This method enables the process of creating a global exception
@ -230,20 +184,5 @@ class Blueprint(RouteMixin):
return decorator return decorator
def static(self, uri, file_or_directory, *args, **kwargs): def _generate_name(self, handler, name: str) -> str:
"""Create a blueprint static route from a decorated function. return f"{self.name}.{name or handler.__name__}"
: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)

19
sanic/mixins/base.py Normal file
View File

@ -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):
...

View File

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

View File

@ -1,25 +1,31 @@
from functools import partial from functools import partial
from inspect import signature from inspect import signature
from typing import List, Set from pathlib import PurePath
from typing import List, Set, Union
import websockets import websockets
from sanic_routing.route import Route from sanic_routing.route import Route
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.models.futures import FutureRoute from sanic.models.futures import FutureRoute, FutureStatic
from sanic.views import CompositionView from sanic.views import CompositionView
class RouteMixin: class RouteMixin:
def __init__(self) -> None: def __init__(self, *args, **kwargs) -> None:
self._future_routes: Set[Route] = set() self._future_routes: Set[FutureRoute] = set()
self._future_websocket_routes: Set[Route] = set() self._future_statics: Set[FutureStatic] = set()
self.name = ""
self.strict_slashes = False
def _apply_route(self, route: FutureRoute) -> Route: def _apply_route(self, route: FutureRoute) -> Route:
raise NotImplementedError raise NotImplementedError
def _route( def _apply_static(self, static: FutureStatic) -> Route:
raise NotImplementedError
def route(
self, self,
uri, uri,
methods=frozenset({"GET"}), methods=frozenset({"GET"}),
@ -133,30 +139,6 @@ class RouteMixin:
return decorator 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( def add_route(
self, self,
handler, handler,
@ -435,7 +417,7 @@ class RouteMixin:
:param version: Blueprint Version :param version: Blueprint Version
:param name: Unique name to identify the Websocket Route :param name: Unique name to identify the Websocket Route
""" """
return self._route( return self.route(
uri=uri, uri=uri,
host=host, host=host,
methods=None, methods=None,
@ -474,11 +456,8 @@ class RouteMixin:
be used with :func:`url_for` be used with :func:`url_for`
:return: Objected decorated by :func:`websocket` :return: Objected decorated by :func:`websocket`
""" """
if strict_slashes is None:
strict_slashes = self.strict_slashes
return self.websocket( return self.websocket(
uri, uri=uri,
host=host, host=host,
strict_slashes=strict_slashes, strict_slashes=strict_slashes,
subprotocols=subprotocols, subprotocols=subprotocols,
@ -486,5 +465,69 @@ class RouteMixin:
name=name, name=name,
)(handler) )(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: def _generate_name(self, handler, name: str) -> str:
return name or handler.__name__ return name or handler.__name__

View File

@ -18,10 +18,20 @@ FutureRoute = namedtuple(
FutureListener = namedtuple( FutureListener = namedtuple(
"FutureListener", ["handler", "uri", "methods", "host"] "FutureListener", ["handler", "uri", "methods", "host"]
) )
FutureMiddleware = namedtuple( FutureMiddleware = namedtuple("FutureMiddleware", ["middleware", "attach_to"])
"FutureMiddleware", ["middleware", "args", "kwargs"]
)
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"]) FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
FutureStatic = namedtuple( 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",
],
) )

View File

@ -16,6 +16,7 @@ from sanic.exceptions import (
) )
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger from sanic.log import error_logger
from sanic.models.futures import FutureStatic
from sanic.response import HTTPResponse, file, file_stream from sanic.response import HTTPResponse, file, file_stream
@ -112,16 +113,7 @@ async def _static_request_handler(
def register( def register(
app, app,
uri: str, static: FutureStatic,
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,
): ):
# TODO: Though sanic is not a file server, I feel like we should at least # 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 # make a good effort here. Modified-since is nice, but we could
@ -152,38 +144,42 @@ def register(
:rtype: List[sanic.router.Route] :rtype: List[sanic.router.Route]
""" """
if isinstance(file_or_directory, bytes): if isinstance(static.file_or_directory, bytes):
file_or_directory = file_or_directory.decode("utf-8") file_or_directory = static.file_or_directory.decode("utf-8")
elif isinstance(file_or_directory, PurePath): elif isinstance(static.file_or_directory, PurePath):
file_or_directory = str(file_or_directory) file_or_directory = str(static.file_or_directory)
elif not isinstance(file_or_directory, str): elif not isinstance(static.file_or_directory, str):
raise ValueError("Invalid file path string.") 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, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
if not path.isfile(file_or_directory): if not path.isfile(file_or_directory):
uri += "<file_uri:" + pattern + ">" uri += "<file_uri:" + static.pattern + ">"
# special prefix for static files # special prefix for static files
if not name.startswith("_static_"): if not static.name.startswith("_static_"):
name = f"_static_{name}" name = f"_static_{static.name}"
_handler = wraps(_static_request_handler)( _handler = wraps(_static_request_handler)(
partial( partial(
_static_request_handler, _static_request_handler,
file_or_directory, file_or_directory,
use_modified_since, static.use_modified_since,
use_content_range, static.use_content_range,
stream_large_files, static.stream_large_files,
content_type=content_type, content_type=static.content_type,
) )
) )
_routes, _ = app.route( _routes, _ = app.route(
uri, uri=uri,
methods=["GET", "HEAD"], methods=["GET", "HEAD"],
name=name, name=name,
host=host, host=static.host,
strict_slashes=strict_slashes, strict_slashes=static.strict_slashes,
)(_handler) )(_handler)
return _routes return _routes