Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add route context #2302

Merged
merged 15 commits into from Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
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