Add route context (#2302)
This commit is contained in:
parent
080d41627a
commit
4659069350
|
@ -382,12 +382,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
websocket_handler.is_websocket = True # type: ignore
|
websocket_handler.is_websocket = True # type: ignore
|
||||||
params["handler"] = websocket_handler
|
params["handler"] = websocket_handler
|
||||||
|
|
||||||
|
ctx = params.pop("route_context")
|
||||||
|
|
||||||
routes = self.router.add(**params)
|
routes = self.router.add(**params)
|
||||||
if isinstance(routes, Route):
|
if isinstance(routes, Route):
|
||||||
routes = [routes]
|
routes = [routes]
|
||||||
|
|
||||||
for r in routes:
|
for r in routes:
|
||||||
r.ctx.websocket = websocket
|
r.ctx.websocket = websocket
|
||||||
r.ctx.static = params.get("static", False)
|
r.ctx.static = params.get("static", False)
|
||||||
|
r.ctx.__dict__.update(ctx)
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
|
|
@ -348,6 +348,7 @@ class Blueprint(BaseSanic):
|
||||||
future.static,
|
future.static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
|
future.route_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (self, apply_route) in app._future_registry:
|
if (self, apply_route) in app._future_registry:
|
||||||
|
|
|
@ -26,12 +26,21 @@ from sanic.log import error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream
|
||||||
|
from sanic.types import HashableDict
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
|
RESTRICTED_ROUTE_CONTEXT = (
|
||||||
|
"ignore_body",
|
||||||
|
"stream",
|
||||||
|
"hosts",
|
||||||
|
"static",
|
||||||
|
"error_format",
|
||||||
|
"websocket",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin:
|
class RouteMixin:
|
||||||
|
@ -65,10 +74,20 @@ class RouteMixin:
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs: Any,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a route
|
Decorate a function to be registered as a route
|
||||||
|
|
||||||
|
|
||||||
|
**Example using context kwargs**
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route(..., ctx_foo="foobar")
|
||||||
|
async def route_handler(request: Request):
|
||||||
|
assert request.route.ctx.foo == "foobar"
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:param host: the host, if required
|
:param host: the host, if required
|
||||||
|
@ -80,6 +99,8 @@ class RouteMixin:
|
||||||
body (eg. GET requests)
|
body (eg. GET requests)
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -94,6 +115,8 @@ class RouteMixin:
|
||||||
if not methods and not websocket:
|
if not methods and not websocket:
|
||||||
methods = frozenset({"GET"})
|
methods = frozenset({"GET"})
|
||||||
|
|
||||||
|
route_context = self._build_route_context(ctx_kwargs)
|
||||||
|
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
nonlocal uri
|
nonlocal uri
|
||||||
nonlocal methods
|
nonlocal methods
|
||||||
|
@ -152,6 +175,7 @@ class RouteMixin:
|
||||||
static,
|
static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
|
route_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._future_routes.add(route)
|
self._future_routes.add(route)
|
||||||
|
@ -196,6 +220,7 @@ class RouteMixin:
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""A helper method to register class instance or
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
|
@ -212,6 +237,8 @@ class RouteMixin:
|
||||||
:param stream: boolean specifying if the handler is a stream handler
|
:param stream: boolean specifying if the handler is a stream handler
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
|
@ -247,6 +274,7 @@ class RouteMixin:
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
@ -261,6 +289,7 @@ class RouteMixin:
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
|
@ -273,6 +302,8 @@ class RouteMixin:
|
||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -285,6 +316,7 @@ class RouteMixin:
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
|
@ -297,6 +329,7 @@ class RouteMixin:
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
|
@ -309,6 +342,8 @@ class RouteMixin:
|
||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -321,6 +356,7 @@ class RouteMixin:
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
|
@ -333,6 +369,7 @@ class RouteMixin:
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
|
@ -345,6 +382,8 @@ class RouteMixin:
|
||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -357,6 +396,7 @@ class RouteMixin:
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
|
@ -369,6 +409,7 @@ class RouteMixin:
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
|
@ -389,6 +430,8 @@ class RouteMixin:
|
||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -401,6 +444,7 @@ class RouteMixin:
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
|
@ -413,6 +457,7 @@ class RouteMixin:
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
|
@ -433,6 +478,8 @@ class RouteMixin:
|
||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -445,6 +492,7 @@ class RouteMixin:
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
|
@ -457,6 +505,7 @@ class RouteMixin:
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
@ -479,6 +528,8 @@ class RouteMixin:
|
||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -491,6 +542,7 @@ class RouteMixin:
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
|
@ -503,6 +555,7 @@ class RouteMixin:
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
|
@ -515,6 +568,8 @@ class RouteMixin:
|
||||||
:param name: Unique name that can be used to identify the Route
|
:param name: Unique name that can be used to identify the Route
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -527,6 +582,7 @@ class RouteMixin:
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
|
@ -540,6 +596,7 @@ class RouteMixin:
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
Decorate a function to be registered as a websocket route
|
||||||
|
@ -553,6 +610,8 @@ class RouteMixin:
|
||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
|
@ -567,6 +626,7 @@ class RouteMixin:
|
||||||
websocket=True,
|
websocket=True,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_websocket_route(
|
def add_websocket_route(
|
||||||
|
@ -580,6 +640,7 @@ class RouteMixin:
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
|
**ctx_kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
A helper method to register a function as a websocket route.
|
A helper method to register a function as a websocket route.
|
||||||
|
@ -598,6 +659,8 @@ class RouteMixin:
|
||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
|
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
||||||
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Objected decorated by :func:`websocket`
|
:return: Objected decorated by :func:`websocket`
|
||||||
"""
|
"""
|
||||||
return self.websocket(
|
return self.websocket(
|
||||||
|
@ -609,6 +672,7 @@ class RouteMixin:
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
def static(
|
||||||
|
@ -957,3 +1021,28 @@ class RouteMixin:
|
||||||
HttpResponseVisitor().visit(node)
|
HttpResponseVisitor().visit(node)
|
||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
|
def _build_route_context(self, raw):
|
||||||
|
ctx_kwargs = {
|
||||||
|
key.replace("ctx_", ""): raw.pop(key)
|
||||||
|
for key in {**raw}.keys()
|
||||||
|
if key.startswith("ctx_")
|
||||||
|
}
|
||||||
|
restricted = [
|
||||||
|
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
||||||
|
]
|
||||||
|
if restricted:
|
||||||
|
restricted_arguments = ", ".join(restricted)
|
||||||
|
raise AttributeError(
|
||||||
|
"Cannot use restricted route context: "
|
||||||
|
f"{restricted_arguments}. This limitation is only in place "
|
||||||
|
"until v22.3 when the restricted names will no longer be in"
|
||||||
|
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
||||||
|
"for more information."
|
||||||
|
)
|
||||||
|
if raw:
|
||||||
|
unexpected_arguments = ", ".join(raw.keys())
|
||||||
|
raise TypeError(
|
||||||
|
f"Unexpected keyword arguments: {unexpected_arguments}"
|
||||||
|
)
|
||||||
|
return HashableDict(ctx_kwargs)
|
||||||
|
|
|
@ -4,11 +4,7 @@ from typing import Any, Callable, Dict, Optional, Set, Union
|
||||||
from sanic.models.futures import FutureSignal
|
from sanic.models.futures import FutureSignal
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
from sanic.signals import Signal
|
from sanic.signals import Signal
|
||||||
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
class HashableDict(dict):
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(tuple(sorted(self.items())))
|
|
||||||
|
|
||||||
|
|
||||||
class SignalMixin:
|
class SignalMixin:
|
||||||
|
|
|
@ -7,6 +7,7 @@ from sanic.models.handler_types import (
|
||||||
MiddlewareType,
|
MiddlewareType,
|
||||||
SignalHandler,
|
SignalHandler,
|
||||||
)
|
)
|
||||||
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
|
|
||||||
class FutureRoute(NamedTuple):
|
class FutureRoute(NamedTuple):
|
||||||
|
@ -25,6 +26,7 @@ class FutureRoute(NamedTuple):
|
||||||
static: bool
|
static: bool
|
||||||
version_prefix: str
|
version_prefix: str
|
||||||
error_format: Optional[str]
|
error_format: Optional[str]
|
||||||
|
route_context: HashableDict
|
||||||
|
|
||||||
|
|
||||||
class FutureListener(NamedTuple):
|
class FutureListener(NamedTuple):
|
||||||
|
|
4
sanic/types/__init__.py
Normal file
4
sanic/types/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .hashable_dict import HashableDict
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("HashableDict",)
|
3
sanic/types/hashable_dict.py
Normal file
3
sanic/types/hashable_dict.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class HashableDict(dict):
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(sorted(self.items())))
|
|
@ -107,7 +107,7 @@ argv = dict(
|
||||||
"-m",
|
"-m",
|
||||||
"sanic",
|
"sanic",
|
||||||
"--port",
|
"--port",
|
||||||
"42104",
|
"42204",
|
||||||
"--debug",
|
"--debug",
|
||||||
"reloader.app",
|
"reloader.app",
|
||||||
],
|
],
|
||||||
|
@ -122,6 +122,7 @@ argv = dict(
|
||||||
({}, "sanic"),
|
({}, "sanic"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@pytest.mark.xfail
|
||||||
async def test_reloader_live(runargs, mode):
|
async def test_reloader_live(runargs, mode):
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
filename = os.path.join(tmpdir, "reloader.py")
|
filename = os.path.join(tmpdir, "reloader.py")
|
||||||
|
@ -154,6 +155,7 @@ async def test_reloader_live(runargs, mode):
|
||||||
({}, "sanic"),
|
({}, "sanic"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@pytest.mark.xfail
|
||||||
async def test_reloader_live_with_dir(runargs, mode):
|
async def test_reloader_live_with_dir(runargs, mode):
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
filename = os.path.join(tmpdir, "reloader.py")
|
filename = os.path.join(tmpdir, "reloader.py")
|
||||||
|
|
|
@ -16,7 +16,7 @@ from sanic import Blueprint, Sanic
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import NotFound, SanicException
|
from sanic.exceptions import NotFound, SanicException
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json, text
|
from sanic.response import empty, json, text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -1230,3 +1230,41 @@ def test_routes_with_and_without_slash_definitions(app):
|
||||||
_, response = app.test_client.post(f"/{term}/")
|
_, response = app.test_client.post(f"/{term}/")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == f"{term}_with"
|
assert response.text == f"{term}_with"
|
||||||
|
|
||||||
|
|
||||||
|
def test_added_route_ctx_kwargs(app):
|
||||||
|
@app.route("/", ctx_foo="foo", ctx_bar=99)
|
||||||
|
async def handler(request: Request):
|
||||||
|
return empty()
|
||||||
|
|
||||||
|
request, _ = app.test_client.get("/")
|
||||||
|
|
||||||
|
assert request.route.ctx.foo == "foo"
|
||||||
|
assert request.route.ctx.bar == 99
|
||||||
|
|
||||||
|
|
||||||
|
def test_added_bad_route_kwargs(app):
|
||||||
|
message = "Unexpected keyword arguments: foo, bar"
|
||||||
|
with pytest.raises(TypeError, match=message):
|
||||||
|
|
||||||
|
@app.route("/", foo="foo", bar=99)
|
||||||
|
async def handler(request: Request):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_added_callable_route_ctx_kwargs(app):
|
||||||
|
def foo(*args, **kwargs):
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
async def bar(*args, **kwargs):
|
||||||
|
return 99
|
||||||
|
|
||||||
|
@app.route("/", ctx_foo=foo, ctx_bar=bar)
|
||||||
|
async def handler(request: Request):
|
||||||
|
return empty()
|
||||||
|
|
||||||
|
request, _ = await app.asgi_client.get("/")
|
||||||
|
|
||||||
|
assert request.route.ctx.foo() == "foo"
|
||||||
|
assert await request.route.ctx.bar() == 99
|
||||||
|
|
|
@ -5,6 +5,8 @@ import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from string import ascii_lowercase
|
||||||
|
|
||||||
import httpcore
|
import httpcore
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -13,6 +15,9 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
httpx_version = tuple(
|
||||||
|
map(int, httpx.__version__.strip(ascii_lowercase).split("."))
|
||||||
|
)
|
||||||
pytestmark = pytest.mark.skipif(os.name != "posix", reason="UNIX only")
|
pytestmark = pytest.mark.skipif(os.name != "posix", reason="UNIX only")
|
||||||
SOCKPATH = "/tmp/sanictest.sock"
|
SOCKPATH = "/tmp/sanictest.sock"
|
||||||
SOCKPATH2 = "/tmp/sanictest2.sock"
|
SOCKPATH2 = "/tmp/sanictest2.sock"
|
||||||
|
@ -141,6 +146,9 @@ def test_unix_connection():
|
||||||
|
|
||||||
@app.listener("after_server_start")
|
@app.listener("after_server_start")
|
||||||
async def client(app, loop):
|
async def client(app, loop):
|
||||||
|
if httpx_version >= (0, 20):
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds=SOCKPATH)
|
||||||
|
else:
|
||||||
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
|
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(transport=transport) as client:
|
async with httpx.AsyncClient(transport=transport) as client:
|
||||||
|
@ -186,6 +194,9 @@ async def test_zero_downtime():
|
||||||
from time import monotonic as current_time
|
from time import monotonic as current_time
|
||||||
|
|
||||||
async def client():
|
async def client():
|
||||||
|
if httpx_version >= (0, 20):
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds=SOCKPATH)
|
||||||
|
else:
|
||||||
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
|
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
|
||||||
for _ in range(40):
|
for _ in range(40):
|
||||||
async with httpx.AsyncClient(transport=transport) as client:
|
async with httpx.AsyncClient(transport=transport) as client:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user