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

View File

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

View File

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

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

View File

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

View File

@ -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 += "<file_uri:" + pattern + ">"
uri += "<file_uri:" + static.pattern + ">"
# 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