Merge in latest from sanic-routing branch

This commit is contained in:
Adam Hopkins
2021-02-15 17:20:07 +02:00
44 changed files with 1299 additions and 609 deletions

View File

@@ -25,27 +25,24 @@ from typing import (
)
from urllib.parse import urlencode, urlunparse
from sanic_routing.route import Route
from sanic_routing.exceptions import FinalizationError # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.asgi import ASGIApp
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.config import BASE_LOGO, Config
from sanic.exceptions import (
InvalidUsage,
NotFound,
SanicException,
ServerError,
URLBuildError,
)
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.mixins.listeners import ListenerEvent
from sanic.models.futures import (
FutureException,
FutureListener,
@@ -68,9 +65,7 @@ from sanic.static import register as static_register
from sanic.websocket import ConnectionClosed, WebSocketProtocol
class Sanic(
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
):
class Sanic(BaseSanic):
"""
The main application instance
"""
@@ -103,9 +98,7 @@ class Sanic(
self.name = name
self.asgi = False
self.router = router or Router(
exception=NotFound, method_handler_exception=NotFound
)
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
@@ -124,6 +117,9 @@ class Sanic(
self.websocket_tasks: Set[Future] = set()
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
# self.named_request_middleware: Dict[str, MiddlewareType] = {}
# self.named_response_middleware: Dict[str, MiddlewareType] = {}
self._test_manager = None
self._test_client = None
self._asgi_client = None
# Register alternative method names
@@ -135,6 +131,8 @@ class Sanic(
if self.config.REGISTER:
self.__class__.register_app(self)
self.router.ctx.app = self
@property
def loop(self):
"""
@@ -175,10 +173,6 @@ class Sanic(
partial(self._loop_add_task, task)
)
# Decorator
def _apply_listener(self, listener: FutureListener):
return self.register_listener(listener.listener, listener.event)
def register_listener(self, listener: Callable, event: str) -> Any:
"""
Register the listener for a given event.
@@ -197,42 +191,6 @@ class Sanic(
self.listeners[_event].append(listener)
return listener
def _apply_route(self, route: FutureRoute) -> Route:
return self.router.add(**route._asdict())
def _apply_static(self, static: FutureStatic) -> Route:
return static_register(self, static)
def enable_websocket(self, enable: bool = True):
"""
Enable or disable the support for websocket.
Websocket is enabled automatically if websocket routes are
added to the application.
"""
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
self.listener("before_server_stop")(self._cancel_websocket_tasks)
self.websocket_enabled = enable
# Decorator
def _apply_exception_handler(self, handler: FutureException):
"""Decorate a function to be registered as a handler for exceptions
:param exceptions: exceptions
:return: decorated function
"""
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: str = "request"):
"""
Register an application level middleware that will be attached
@@ -288,13 +246,49 @@ class Sanic(
if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware)
# Decorator
def _apply_exception_handler(self, handler: FutureException):
"""Decorate a function to be registered as a handler for exceptions
:param exceptions: exceptions
:return: decorated function
"""
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 _apply_listener(self, listener: FutureListener):
return self.register_listener(listener.listener, listener.event)
def _apply_route(self, route: FutureRoute) -> Route:
params = route._asdict()
websocket = params.pop("websocket", False)
subprotocols = params.pop("subprotocols", None)
if websocket:
self.enable_websocket()
websocket_handler = partial(
self._websocket_handler,
route.handler,
subprotocols=subprotocols,
)
websocket_handler.__name__ = route.handler.__name__ # type: ignore
websocket_handler.is_websocket = True # type: ignore
params["handler"] = websocket_handler
return self.router.add(**params)
def _apply_static(self, static: FutureStatic) -> Route:
return static_register(self, static)
def _apply_middleware(
self,
middleware: FutureMiddleware,
route_names: Optional[List[str]] = None,
):
print(f"{middleware=}")
if route_names:
return self.register_named_middleware(
middleware.middleware, route_names, middleware.attach_to
@@ -304,6 +298,19 @@ class Sanic(
middleware.middleware, middleware.attach_to
)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
Websocket is enabled automatically if websocket routes are
added to the application.
"""
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
self.listener("before_server_stop")(self._cancel_websocket_tasks)
self.websocket_enabled = enable
def blueprint(self, blueprint, **options):
"""Register a blueprint on the application.
@@ -323,6 +330,12 @@ class Sanic(
else:
self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint)
if (
self.strict_slashes is not None
and blueprint.strict_slashes is None
):
blueprint.strict_slashes = self.strict_slashes
blueprint.register(self, options)
def url_for(self, view_name: str, **kwargs):
@@ -351,30 +364,28 @@ class Sanic(
# find the route by the supplied view name
kw: Dict[str, str] = {}
# special static files url_for
if view_name == "static":
kw.update(name=kwargs.pop("name", "static"))
elif view_name.endswith(".static"): # blueprint.static
kwargs.pop("name", None)
if "." not in view_name:
view_name = f"{self.name}.{view_name}"
if view_name.endswith(".static"):
name = kwargs.pop("name", None)
if name:
view_name = view_name.replace("static", name)
kw.update(name=view_name)
uri, route = self.router.find_route_by_view_name(view_name, **kw)
if not (uri and route):
route = self.router.find_route_by_view_name(view_name, **kw)
if not route:
raise URLBuildError(
f"Endpoint with name `{view_name}` was not found"
)
# If the route has host defined, split that off
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
uri = route.path
if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None)
if getattr(route.ctx, "static", None):
filename = kwargs.pop("filename", "")
# it's static folder
if "<file_uri:" in uri:
if "file_uri" in uri:
folder_ = uri.split("<file_uri:", 1)[0]
if folder_.endswith("/"):
folder_ = folder_[:-1]
@@ -382,22 +393,36 @@ class Sanic(
if filename.startswith("/"):
filename = filename[1:]
uri = f"{folder_}/{filename}"
kwargs["file_uri"] = filename
if uri != "/" and uri.endswith("/"):
uri = uri[:-1]
out = uri
if not uri.startswith("/"):
uri = f"/{uri}"
# find all the parameters we will need to build in the URL
matched_params = re.findall(self.router.parameter_pattern, uri)
out = uri
# _method is only a placeholder now, don't know how to support it
kwargs.pop("_method", None)
anchor = kwargs.pop("_anchor", "")
# _external need SERVER_NAME in config or pass _server arg
external = kwargs.pop("_external", False)
host = kwargs.pop("_host", None)
external = kwargs.pop("_external", False) or bool(host)
scheme = kwargs.pop("_scheme", "")
if route.ctx.hosts and external:
if not host and len(route.ctx.hosts) > 1:
raise ValueError(
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
)
elif host and host not in route.ctx.hosts:
raise ValueError(
f"Requested host ({host}) is not available for this "
f"route: {route.ctx.hosts}"
)
elif not host:
host = list(route.ctx.hosts)[0]
if scheme and not external:
raise ValueError("When specifying _scheme, _external must be True")
@@ -415,44 +440,44 @@ class Sanic(
if "://" in netloc[:8]:
netloc = netloc.split("://", 1)[-1]
for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string(match)
# find all the parameters we will need to build in the URL
# matched_params = re.findall(self.router.parameter_pattern, uri)
route.finalize()
for param_info in route.params.values():
# name, _type, pattern = self.router.parse_parameter_string(match)
# we only want to match against each individual parameter
specific_pattern = f"^{pattern}$"
supplied_param = None
if name in kwargs:
supplied_param = kwargs.get(name)
del kwargs[name]
else:
try:
supplied_param = str(kwargs.pop(param_info.name))
except KeyError:
raise URLBuildError(
f"Required parameter `{name}` was not passed to url_for"
f"Required parameter `{param_info.name}` was not "
"passed to url_for"
)
supplied_param = str(supplied_param)
# determine if the parameter supplied by the caller passes the test
# in the URL
passes_pattern = re.match(specific_pattern, supplied_param)
if not passes_pattern:
if _type != str:
type_name = _type.__name__
msg = (
f'Value "{supplied_param}" '
f"for parameter `{name}` does not "
f"match pattern for type `{type_name}`: {pattern}"
)
else:
msg = (
f'Value "{supplied_param}" for parameter `{name}` '
f"does not satisfy pattern {pattern}"
)
raise URLBuildError(msg)
# determine if the parameter supplied by the caller
# passes the test in the URL
if param_info.pattern:
passes_pattern = param_info.pattern.match(supplied_param)
if not passes_pattern:
if param_info.cast != str:
msg = (
f'Value "{supplied_param}" '
f"for parameter `{param_info.name}` does "
"not match pattern for type "
f"`{param_info.cast.__name__}`: "
f"{param_info.pattern.pattern}"
)
else:
msg = (
f'Value "{supplied_param}" for parameter '
f"`{param_info.name}` does not satisfy "
f"pattern {param_info.pattern.pattern}"
)
raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value
replacement_regex = f"(<{name}.*?>)"
replacement_regex = f"(<{param_info.name}.*?>)"
out = re.sub(replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring
@@ -545,14 +570,13 @@ class Sanic(
# Fetch handler from router
(
handler,
args,
kwargs,
uri,
name,
endpoint,
ignore_body,
) = self.router.get(request)
request.name = name
request._match_info = kwargs
if (
request.stream
@@ -578,7 +602,7 @@ class Sanic(
# Execute Handler
# -------------------------------------------- #
request.uri_template = uri
request.uri_template = f"/{uri}"
if handler is None:
raise ServerError(
(
@@ -587,10 +611,10 @@ class Sanic(
)
)
request.endpoint = endpoint
request.endpoint = request.name
# Run response handler
response = handler(request, *args, **kwargs)
response = handler(request, **kwargs)
if isawaitable(response):
response = await response
if response:
@@ -615,26 +639,60 @@ class Sanic(
except CancelledError:
raise
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
await self.handle_exception(request, e)
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
protocol.app = self
ws = await protocol.websocket_handshake(request, subprotocols)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
# -------------------------------------------------------------------- #
# Testing
# -------------------------------------------------------------------- #
@property
def test_client(self):
def test_client(self): # noqa
if self._test_client:
return self._test_client
elif self._test_manager:
return self._test_manager.test_client
from sanic_testing.testing import SanicTestClient # type: ignore
self._test_client = SanicTestClient(self)
return self._test_client
@property
def asgi_client(self):
def asgi_client(self): # noqa
"""
A testing client that uses ASGI to reach into the application to
execute hanlers.
@@ -644,6 +702,8 @@ class Sanic(
"""
if self._asgi_client:
return self._asgi_client
elif self._test_manager:
return self._test_manager.asgi_client
from sanic_testing.testing import SanicASGITestClient # type: ignore
self._asgi_client = SanicASGITestClient(self)
@@ -915,7 +975,11 @@ class Sanic(
):
"""Helper function used by `run` and `create_server`."""
self.router.finalize()
try:
self.router.finalize()
except FinalizationError as e:
if not Sanic.test_mode:
raise e
if isinstance(ssl, dict):
# try common aliaseses
@@ -950,9 +1014,7 @@ class Sanic(
"backlog": backlog,
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, reverse in (
("before_server_start", "before_start", False),
@@ -1014,40 +1076,6 @@ class Sanic(
for task in app.websocket_tasks:
task.cancel()
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
protocol.app = self
ws = await protocol.websocket_handshake(request, subprotocols)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #
@@ -1057,9 +1085,10 @@ class Sanic(
To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest
/"""
"""
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
asgi_app = self._asgi_app
await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable

View File

@@ -131,6 +131,7 @@ class Lifespan:
in sequence since the ASGI lifespan protocol only supports a single
startup event.
"""
self.asgi_app.sanic_app.router.finalize()
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_start", []
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])

36
sanic/base.py Normal file
View File

@@ -0,0 +1,36 @@
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
from sanic.mixins.middleware import MiddlewareMixin
from sanic.mixins.routes import RouteMixin
class Base(type):
def __new__(cls, name, bases, attrs):
init = attrs.get("__init__")
def __init__(self, *args, **kwargs):
nonlocal init
nonlocal name
bases = [
b for base in type(self).__bases__ for b in base.__bases__
]
for base in bases:
base.__init__(self, *args, **kwargs)
if init:
init(self, *args, **kwargs)
attrs["__init__"] = __init__
return type.__new__(cls, name, bases, attrs)
class BaseSanic(
RouteMixin,
MiddlewareMixin,
ListenerMixin,
ExceptionMixin,
metaclass=Base,
):
...

View File

@@ -1,18 +1,12 @@
from collections import defaultdict, namedtuple
from typing import Iterable, Optional
from collections import defaultdict
from typing import Optional
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
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 FutureRoute, FutureStatic
class Blueprint(
BaseMixin, RouteMixin, MiddlewareMixin, ListenerMixin, ExceptionMixin
):
class Blueprint(BaseSanic):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
@@ -122,20 +116,35 @@ class Blueprint(
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
strict_slashes = (
self.strict_slashes
if future.strict_slashes is None
and self.strict_slashes is not None
else future.strict_slashes
)
name = app._generate_name(future.name)
apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
future.host or self.host,
future.strict_slashes,
strict_slashes,
future.stream,
future.version or self.version,
future.name,
name,
future.ignore_body,
future.websocket,
future.subprotocols,
future.unquote,
future.static,
)
route = app._apply_route(apply_route)
routes.append(route)
operation = (
routes.extend if isinstance(route, list) else routes.append
)
operation(route)
# Static Files
for future in self._future_statics:
@@ -148,8 +157,9 @@ class Blueprint(
route_names = [route.name for route in routes if route]
# Middleware
for future in self._future_middleware:
app._apply_middleware(future, route_names)
if route_names:
for future in self._future_middleware:
app._apply_middleware(future, route_names)
# Exceptions
for future in self._future_exceptions:
@@ -158,6 +168,3 @@ class Blueprint(
# Event listeners
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

@@ -1,19 +0,0 @@
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

@@ -1,5 +1,3 @@
from enum import Enum, auto
from functools import partial
from typing import Set
from sanic.models.futures import FutureException
@@ -29,6 +27,9 @@ class ExceptionMixin:
nonlocal apply
nonlocal exceptions
if isinstance(exceptions[0], list):
exceptions = tuple(*exceptions)
future_exception = FutureException(handler, exceptions)
self._future_exceptions.add(future_exception)
if apply:

View File

@@ -1,6 +1,6 @@
from enum import Enum, auto
from functools import partial
from typing import Any, Callable, Coroutine, Optional, Set, Union
from typing import Any, Callable, Coroutine, List, Optional, Set, Union
from sanic.models.futures import FutureListener
@@ -17,7 +17,7 @@ class ListenerEvent(str, Enum):
class ListenerMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_listeners: Set[FutureListener] = set()
self._future_listeners: List[FutureListener] = list()
def _apply_listener(self, listener: FutureListener):
raise NotImplementedError
@@ -51,7 +51,7 @@ class ListenerMixin:
nonlocal apply
future_listener = FutureListener(listener, event)
self._future_listeners.add(future_listener)
self._future_listeners.append(future_listener)
if apply:
self._apply_listener(future_listener)
return listener

View File

@@ -1,12 +1,12 @@
from functools import partial
from typing import Set
from typing import List
from sanic.models.futures import FutureMiddleware
class MiddlewareMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_middleware: Set[FutureMiddleware] = set()
self._future_middleware: List[FutureMiddleware] = list()
def _apply_middleware(self, middleware: FutureMiddleware):
raise NotImplementedError
@@ -30,7 +30,7 @@ class MiddlewareMixin:
nonlocal apply
future_middleware = FutureMiddleware(middleware, attach_to)
self._future_middleware.add(future_middleware)
self._future_middleware.append(future_middleware)
if apply:
self._apply_middleware(future_middleware)
return middleware

View File

@@ -1,9 +1,8 @@
from functools import partial
from inspect import signature
from pathlib import PurePath
from typing import Iterable, List, Optional, Set, Union
from sanic_routing.route import Route
from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS
from sanic.models.futures import FutureRoute, FutureStatic
@@ -36,6 +35,8 @@ class RouteMixin:
apply: bool = True,
subprotocols: Optional[List[str]] = None,
websocket: bool = False,
unquote: bool = False,
static: bool = False,
):
"""
Decorate a function to be registered as a route
@@ -52,9 +53,6 @@ class RouteMixin:
:return: tuple of routes, decorated function
"""
if websocket:
self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith("/"):
@@ -63,6 +61,9 @@ class RouteMixin:
if strict_slashes is None:
strict_slashes = self.strict_slashes
if not methods and not websocket:
methods = frozenset({"GET"})
def decorator(handler):
nonlocal uri
nonlocal methods
@@ -74,39 +75,43 @@ class RouteMixin:
nonlocal ignore_body
nonlocal subprotocols
nonlocal websocket
nonlocal static
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
_, handler = handler
if websocket:
websocket_handler = partial(
self._websocket_handler,
handler,
subprotocols=subprotocols,
)
websocket_handler.__name__ = (
"websocket_handler_" + handler.__name__
)
websocket_handler.is_websocket = True
handler = websocket_handler
name = self._generate_name(name, handler)
# TODO:
# - THink this thru.... do we want all routes namespaced?
# -
name = self._generate_name(handler, name)
if isinstance(host, str):
host = frozenset([host])
elif host and not isinstance(host, frozenset):
try:
host = frozenset(host)
except TypeError:
raise ValueError(
"Expected either string or Iterable of host strings, "
"not %s" % host
)
if isinstance(subprotocols, (list, tuple, set)):
subprotocols = frozenset(subprotocols)
route = FutureRoute(
handler,
uri,
frozenset(methods),
None if websocket else frozenset([x.upper() for x in methods]),
host,
strict_slashes,
stream,
version,
name,
ignore_body,
websocket,
subprotocols,
unquote,
static,
)
self._future_routes.add(route)
@@ -441,6 +446,7 @@ class RouteMixin:
subprotocols: Optional[List[str]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
apply: bool = True,
):
"""
Decorate a function to be registered as a websocket route
@@ -543,12 +549,16 @@ class RouteMixin:
:rtype: List[sanic.router.Route]
"""
if not name.startswith(self.name + "."):
name = f"{self.name}.{name}"
name = self._generate_name(name)
if strict_slashes is None and self.strict_slashes is not None:
strict_slashes = self.strict_slashes
if not isinstance(file_or_directory, (str, bytes, PurePath)):
raise ValueError(
f"Static route must be a valid path, not {file_or_directory}"
)
static = FutureStatic(
uri,
file_or_directory,
@@ -566,5 +576,29 @@ class RouteMixin:
if apply:
self._apply_static(static)
def _generate_name(self, handler, name: str) -> str:
return name or handler.__name__
def _generate_name(self, *objects) -> str:
name = None
for obj in objects:
if obj:
if isinstance(obj, str):
name = obj
break
try:
name = obj.name
except AttributeError:
try:
name = obj.__name__
except AttributeError:
continue
else:
break
if not name:
raise Exception("...")
if not name.startswith(f"{self.name}."):
name = f"{self.name}.{name}"
return name

View File

@@ -13,6 +13,10 @@ FutureRoute = namedtuple(
"version",
"name",
"ignore_body",
"websocket",
"subprotocols",
"unquote",
"static",
],
)
FutureListener = namedtuple("FutureListener", ["listener", "event"])

View File

@@ -87,6 +87,7 @@ class Request:
"_port",
"_remote_addr",
"_socket",
"_match_info",
"app",
"body",
"conn_info",
@@ -147,6 +148,7 @@ class Request:
self.uri_template: Optional[str] = None
self.request_middleware_started = False
self._cookies: Dict[str, str] = {}
self._match_info = {}
self.stream: Optional[Http] = None
self.endpoint: Optional[str] = None
@@ -455,7 +457,7 @@ class Request:
"""
:return: matched info after resolving route
"""
return self.app.router.get(self)[2]
return self._match_info
# Transport properties (obtained from local interface only)

View File

@@ -1,14 +1,22 @@
from functools import lru_cache
from typing import Any, Dict, Iterable, Optional, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from sanic_routing import BaseRouter
from sanic_routing.route import Route
from sanic_routing import BaseRouter # type: ignore
from sanic_routing.exceptions import NoMethod # type: ignore
from sanic_routing.exceptions import (
NotFound as RoutingNotFound, # type: ignore
)
from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.handlers import RouteHandler
from sanic.request import Request
ROUTER_CACHE_SIZE = 1024
class Router(BaseRouter):
"""
The router implementation responsible for routing a :class:`Request` object
@@ -18,18 +26,38 @@ class Router(BaseRouter):
DEFAULT_METHOD = "GET"
ALLOWED_METHODS = HTTP_METHODS
@lru_cache
def get(
self, request: Request
) -> Tuple[
RouteHandler,
Tuple[Any, ...],
Dict[str, Any],
str,
str,
Optional[str],
bool,
]:
# Putting the lru_cache on Router.get() performs better for the benchmarsk
# at tests/benchmark/test_route_resolution_benchmark.py
# However, overall application performance is significantly improved
# with the lru_cache on this method.
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(
self, path, method, host
) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool,]:
try:
route, handler, params = self.resolve(
path=path,
method=method,
extra={"host": host},
)
except RoutingNotFound as e:
raise NotFound("Requested URL {} not found".format(e.path))
except NoMethod as e:
raise MethodNotSupported(
"Method {} not allowed for URL {}".format(method, path),
method=method,
allowed_methods=e.allowed_methods,
)
return (
handler,
params,
route.path,
route.name,
route.ctx.ignore_body,
)
def get(self, request: Request):
"""
Retrieve a `Route` object containg the details about how to handle
a response for a given request
@@ -41,23 +69,8 @@ class Router(BaseRouter):
:rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str,
Optional[str], bool, ]
"""
route, handler, params = self.resolve(
path=request.path,
method=request.method,
)
# TODO: Implement response
# - args,
# - endpoint,
return (
handler,
(),
params,
route.path,
route.name,
None,
route.ctx.ignore_body,
return self._get(
request.path, request.method, request.headers.get("host")
)
def add(
@@ -65,13 +78,15 @@ class Router(BaseRouter):
uri: str,
methods: Iterable[str],
handler: RouteHandler,
host: Optional[str] = None,
host: Optional[Union[str, Iterable[str]]] = None,
strict_slashes: bool = False,
stream: bool = False,
ignore_body: bool = False,
version: Union[str, float, int] = None,
name: Optional[str] = None,
) -> Route:
unquote: bool = False,
static: bool = False,
) -> Union[Route, List[Route]]:
"""
Add a handler to the router
@@ -99,19 +114,93 @@ class Router(BaseRouter):
:return: the route object
:rtype: Route
"""
# TODO: Implement
# - host
# - strict_slashes
# - ignore_body
# - stream
if version is not None:
version = str(version).strip("/").lstrip("v")
uri = "/".join([f"/v{version}", uri.lstrip("/")])
route = super().add(
path=uri, handler=handler, methods=methods, name=name
params = dict(
path=uri,
handler=handler,
methods=methods,
name=name,
strict=strict_slashes,
unquote=unquote,
)
route.ctx.ignore_body = ignore_body
route.ctx.stream = stream
if isinstance(host, str):
hosts = [host]
else:
hosts = host or [None] # type: ignore
routes = []
for host in hosts:
if host:
params.update({"requirements": {"host": host}})
route = super().add(**params)
route.ctx.ignore_body = ignore_body
route.ctx.stream = stream
route.ctx.hosts = hosts
route.ctx.static = static
routes.append(route)
if len(routes) == 1:
return routes[0]
return routes
def is_stream_handler(self, request) -> bool:
"""
Handler for request is stream or not.
:param request: Request object
:return: bool
"""
try:
handler = self.get(request)[0]
except (NotFound, MethodNotSupported):
return False
if hasattr(handler, "view_class") and hasattr(
handler.view_class, request.method.lower()
):
handler = getattr(handler.view_class, request.method.lower())
return hasattr(handler, "is_stream")
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name, name=None):
"""
Find a route in the router based on the specified view name.
:param view_name: string of view name to search by
:param kwargs: additional params, usually for static files
:return: tuple containing (uri, Route)
"""
if not view_name:
return None
route = self.name_index.get(view_name)
if not route:
full_name = self.ctx.app._generate_name(view_name)
route = self.name_index.get(full_name)
if not route:
return None
return route
@property
def routes_all(self):
return self.routes
@property
def routes_static(self):
return self.static_routes
@property
def routes_dynamic(self):
return self.dynamic_routes
@property
def routes_regex(self):
return self.regex_routes

View File

@@ -157,11 +157,11 @@ def register(
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += "<file_uri:" + static.pattern + ">"
uri += "/<file_uri>"
# special prefix for static files
if not static.name.startswith("_static_"):
name = f"_static_{static.name}"
# if not static.name.startswith("_static_"):
# name = f"_static_{static.name}"
_handler = wraps(_static_request_handler)(
partial(
@@ -174,11 +174,13 @@ def register(
)
)
_routes, _ = app.route(
route, _ = app.route(
uri=uri,
methods=["GET", "HEAD"],
name=name,
host=static.host,
strict_slashes=static.strict_slashes,
static=True,
)(_handler)
return _routes
return route