Skip to content

Commit

Permalink
Add route context (sanic-org#2302)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored and ChihweiLHBird committed Jun 1, 2022
1 parent 84388b1 commit 16e17a3
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 9 deletions.
4 changes: 4 additions & 0 deletions sanic/app.py
Expand Up @@ -382,12 +382,16 @@ def _apply_route(self, route: FutureRoute) -> List[Route]:
websocket_handler.is_websocket = True # type: ignore
params["handler"] = websocket_handler

ctx = params.pop("route_context")

routes = self.router.add(**params)
if isinstance(routes, Route):
routes = [routes]

for r in routes:
r.ctx.websocket = websocket
r.ctx.static = params.get("static", False)
r.ctx.__dict__.update(ctx)

return routes

Expand Down
1 change: 1 addition & 0 deletions sanic/blueprints.py
Expand Up @@ -348,6 +348,7 @@ def register(self, app, options):
future.static,
version_prefix,
error_format,
future.route_context,
)

if (self, apply_route) in app._future_registry:
Expand Down
89 changes: 89 additions & 0 deletions sanic/mixins/routes.py
Expand Up @@ -26,12 +26,21 @@
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream
from sanic.types import HashableDict
from sanic.views import CompositionView


RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)


class RouteMixin:
Expand Down Expand Up @@ -65,10 +74,20 @@ def route(
static: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper:
"""
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 methods: list or tuple of methods allowed
:param host: the host, if required
Expand All @@ -80,6 +99,8 @@ def route(
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
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
"""

Expand All @@ -94,6 +115,8 @@ def route(
if not methods and not websocket:
methods = frozenset({"GET"})

route_context = self._build_route_context(ctx_kwargs)

def decorator(handler):
nonlocal uri
nonlocal methods
Expand Down Expand Up @@ -152,6 +175,7 @@ def decorator(handler):
static,
version_prefix,
error_format,
route_context,
)

self._future_routes.add(route)
Expand Down Expand Up @@ -196,6 +220,7 @@ def add_route(
stream: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
Expand All @@ -212,6 +237,8 @@ def add_route(
:param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version
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
"""
# Handle HTTPMethodView differently
Expand Down Expand Up @@ -247,6 +274,7 @@ def add_route(
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
return handler

Expand All @@ -261,6 +289,7 @@ def get(
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
Expand All @@ -273,6 +302,8 @@ def get(
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -285,6 +316,7 @@ def get(
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def post(
Expand All @@ -297,6 +329,7 @@ def post(
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
Expand All @@ -309,6 +342,8 @@ def post(
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -321,6 +356,7 @@ def post(
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def put(
Expand All @@ -333,6 +369,7 @@ def put(
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
Expand All @@ -345,6 +382,8 @@ def put(
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -357,6 +396,7 @@ def put(
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def head(
Expand All @@ -369,6 +409,7 @@ def head(
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
Expand All @@ -389,6 +430,8 @@ def head(
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -401,6 +444,7 @@ def head(
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def options(
Expand All @@ -413,6 +457,7 @@ def options(
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
Expand All @@ -433,6 +478,8 @@ def options(
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -445,6 +492,7 @@ def options(
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def patch(
Expand All @@ -457,6 +505,7 @@ def patch(
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
Expand All @@ -479,6 +528,8 @@ def patch(
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -491,6 +542,7 @@ def patch(
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def delete(
Expand All @@ -503,6 +555,7 @@ def delete(
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
Expand All @@ -515,6 +568,8 @@ def delete(
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -527,6 +582,7 @@ def delete(
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def websocket(
Expand All @@ -540,6 +596,7 @@ def websocket(
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
Decorate a function to be registered as a websocket route
Expand All @@ -553,6 +610,8 @@ def websocket(
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
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 self.route(
Expand All @@ -567,6 +626,7 @@ def websocket(
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)

def add_websocket_route(
Expand All @@ -580,6 +640,7 @@ def add_websocket_route(
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
A helper method to register a function as a websocket route.
Expand All @@ -598,6 +659,8 @@ def add_websocket_route(
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
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 self.websocket(
Expand All @@ -609,6 +672,7 @@ def add_websocket_route(
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)

def static(
Expand Down Expand Up @@ -957,3 +1021,28 @@ def visit_Return(self, node: Return) -> Any:
HttpResponseVisitor().visit(node)

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)
6 changes: 1 addition & 5 deletions sanic/mixins/signals.py
Expand Up @@ -4,11 +4,7 @@
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
from sanic.signals import Signal


class HashableDict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))
from sanic.types import HashableDict


class SignalMixin:
Expand Down

0 comments on commit 16e17a3

Please sign in to comment.