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

feat!: Make route handlers functional decorators #3436

Merged
merged 17 commits into from
May 25, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/release-notes/2.x-changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3057,7 +3057,7 @@
:pr: 1647

Dependencies can now be used in the
:class:`~litestar.handlers.websocket_listener` hooks
:func:`~litestar.handlers.websocket_listener` hooks
``on_accept``, ``on_disconnect`` and the ``connection_lifespan`` context
manager. The ``socket`` parameter is therefore also not mandatory anymore in
those callables.
Expand Down Expand Up @@ -3208,7 +3208,7 @@
:issue: 1615

A bug was fixed that would cause a type error when using a
:class:`websocket_listener <litestar.handlers.websocket_listener>`
:func:`websocket_listener <litestar.handlers.websocket_listener>`
in a ``Controller``

.. change:: Add ``connection_accept_handler`` to ``websocket_listener``
Expand All @@ -3217,7 +3217,7 @@
:issue: 1571

Add a new ``connection_accept_handler`` parameter to
:class:`websocket_listener <litestar.handlers.websocket_listener>`,
:func:`websocket_listener <litestar.handlers.websocket_listener>`,
which can be used to customize how a connection is accepted, for example to
add headers or subprotocols

Expand Down Expand Up @@ -3305,7 +3305,7 @@
appropriate event hooks - to use a context manager.

The ``connection_lifespan`` argument was added to the
:class:`WebSocketListener <litestar.handlers.websocket_listener>`, which accepts
:func:`WebSocketListener <litestar.handlers.websocket_listener>`, which accepts
an asynchronous context manager, which can be used to handle the lifespan of
the socket.

Expand Down Expand Up @@ -3419,7 +3419,7 @@
:pr: 1518

Support for DTOs has been added to :class:`WebSocketListener <litestar.handlers.WebsocketListener>` and
:class:`WebSocketListener <litestar.handlers.websocket_listener>`. A ``dto`` and ``return_dto`` parameter has
:func:`WebSocketListener <litestar.handlers.websocket_listener>`. A ``dto`` and ``return_dto`` parameter has
been added, providing the same functionality as their route handler counterparts.

.. change:: DTO based serialization plugin
Expand Down
36 changes: 36 additions & 0 deletions docs/release-notes/whats-new-3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,39 @@ If you were relying on this utility, you can define it yourself as follows:

def is_sync_or_async_generator(obj: Any) -> bool:
return isgeneratorfunction(obj) or isasyncgenfunction(obj)


Removal of semantic HTTP route handler classes
-----------------------------------------------

The semantic ``HTTPRouteHandler`` classes have been removed in favour of functional
decorators. ``route``, ``get``, ``post``, ``patch``, ``put``, ``head`` and ``delete``
are now all decorator functions returning :class:`~.handlers.HTTPRouteHandler`
instances.
provinzkraut marked this conversation as resolved.
Show resolved Hide resolved

As a result, customizing the decorators directly is not possible anymore. Instead, to
use a route handler decorator with a custom route handler class, the ``handler_class``
parameter to the decorator function can be used:

Before:

.. code-block:: python

class my_get_handler(get):
... # custom handler

@my_get_handler()
async def handler() -> Any:
...

After:

.. code-block:: python

class MyHTTPRouteHandler(HTTPRouteHandler):
... # custom handler


@get(handler_class=MyHTTPRouteHandler)
async def handler() -> Any:
...
54 changes: 28 additions & 26 deletions docs/usage/routing/handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ handler :term:`decorators <decorator>` exported from Litestar.
For example:

.. code-block:: python
:caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator`
:caption: Defining a route handler by decorating a function with the :func:`@get() <.handlers.get>` :term:`decorator`

from litestar import get

Expand Down Expand Up @@ -146,12 +146,11 @@ There are several reasons for why this limitation is enforced:
HTTP route handlers
-------------------

The most commonly used route handlers are those that handle HTTP requests and responses.
These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the
:term:`decorator` called :func:`~.handlers.route`:
The :class:`~.handlers.HTTPRouteHandler` is used to handle HTTP requests, and can be
created with the :func:`~.handlers.route` :term:`decorator`:

.. code-block:: python
:caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>`
:caption: Defining a route handler by decorating a function with the :func:`@route() <.handlers.route>`
:term:`decorator`

from litestar import HttpMethod, route
Expand All @@ -160,20 +159,24 @@ These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` c
@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def my_endpoint() -> None: ...

As mentioned above, :func:`@route() <.handlers.route>` is merely an alias for ``HTTPRouteHandler``,
thus the below code is equivalent to the one above:
The same can be achieved without a decorator, by using ``HTTPRouteHandler`` directly:

.. code-block:: python
:caption: Defining a route handler by decorating a function with the
:class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class
:caption: Defining a route handler creating an instance of
:class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>`

from litestar import HttpMethod
from litestar.handlers.http_handlers import HTTPRouteHandler


@HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def my_endpoint() -> None: ...

handler = HTTPRouteHandler(
path="/some-path",
http_method=[HttpMethod.GET, HttpMethod.POST],
fn=my_endpoint
)


Semantic handler :term:`decorators <decorator>`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -189,8 +192,8 @@ which correlates with their name:
* :func:`@post() <.handlers.post>`
* :func:`@put() <.handlers.put>`

These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you cannot configure the
:paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg <argument>`:
These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you don't need to configure
the :paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg <argument>`:

.. dropdown:: Click to see the predefined route handlers

Expand Down Expand Up @@ -240,11 +243,6 @@ These are used exactly like :func:`@route() <.handlers.route>` with the sole exc
@delete(path="/resources/{pk:int}")
async def delete_resource(pk: int) -> None: ...

Although these :term:`decorators <decorator>` are merely subclasses of :class:`~.handlers.HTTPRouteHandler` that pre-set
the :paramref:`~.handlers.HTTPRouteHandler.http_method`, using :func:`@get() <.handlers.get>`,
:func:`@patch() <.handlers.patch>`, :func:`@put() <.handlers.put>`, :func:`@delete() <.handlers.delete>`, or
:func:`@post() <.handlers.post>` instead of :func:`@route() <.handlers.route>` makes the code clearer and simpler.

Furthermore, in the OpenAPI specification each unique combination of HTTP verb (e.g. ``GET``, ``POST``, etc.) and path
is regarded as a distinct `operation <https://spec.openapis.org/oas/latest.html#operation-object>`_\ , and each
operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally
Expand Down Expand Up @@ -277,22 +275,25 @@ A WebSocket connection can be handled with a :func:`@websocket() <.handlers.Webs
await socket.send_json({...})
await socket.close()

The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is an alias of the
:class:`~.handlers.WebsocketRouteHandler` class. Thus, the below code is equivalent to the one above:
The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` can be used to create an instance of
:class:`~.handlers.WebsocketRouteHandler`. Therefore, the below code is equivalent to the one above:

.. code-block:: python
:caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly

from litestar import WebSocket
from litestar.handlers.websocket_handlers import WebsocketRouteHandler


@WebsocketRouteHandler(path="/socket")
async def my_websocket_handler(socket: WebSocket) -> None:
await socket.accept()
await socket.send_json({...})
await socket.close()

my_websocket_handler = WebsocketRouteHandler(
path="/socket",
fn=my_websocket_handler,
)

In difference to HTTP routes handlers, websocket handlers have the following requirements:

#. They **must** declare a ``socket`` :term:`kwarg <argument>`.
Expand Down Expand Up @@ -332,8 +333,8 @@ If you need to write your own ASGI application, you can do so using the :func:`@
)
await response(scope=scope, receive=receive, send=send)

Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator` is an alias of the
:class:`~.handlers.ASGIRouteHandler` class. Thus, the code below is equivalent to the one above:
:func:`@asgi() <.handlers.asgi>` :term:`decorator` can be used to create an instance of
:class:`~.handlers.ASGIRouteHandler`. Therefore, the code below is equivalent to the one above:

.. code-block:: python
:caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly
Expand All @@ -343,8 +344,6 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
from litestar.status_codes import HTTP_400_BAD_REQUEST
from litestar.types import Scope, Receive, Send


@ASGIRouteHandler(path="/my-asgi-app")
async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
if scope["method"] == "GET":
Expand All @@ -356,7 +355,10 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
)
await response(scope=scope, receive=receive, send=send)

Limitations of ASGI route handlers
my_asgi_app = ASGIRouteHandler(path="/my-asgi-app", fn=my_asgi_app)


ASGI route handler considerations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In difference to the other route handlers, the :func:`@asgi() <.handlers.asgi>` route handler accepts only three
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/websockets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exceptions, and parsing incoming and serializing outgoing data. In addition to t
low-level :class:`WebSocket route handler <.handlers.websocket>`, Litestar offers two
high level interfaces:

- :class:`websocket_listener <.handlers.websocket_listener>`
- :func:`websocket_listener <.handlers.websocket_listener>`
- :class:`WebSocketListener <.handlers.WebsocketListener>`


Expand Down Expand Up @@ -38,7 +38,7 @@ type of data which should be received, and it will be converted accordingly.

.. note::
Contrary to WebSocket route handlers, functions decorated with
:class:`websocket_listener <.handlers.websocket_listener>` don't have to be
:func:`websocket_listener <.handlers.websocket_listener>` don't have to be
asynchronous.


Expand Down
6 changes: 3 additions & 3 deletions litestar/channels/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig:
if self._create_route_handlers:
if self._arbitrary_channels_allowed:
path = self._handler_root_path + "{channel_name:str}"
route_handlers = [WebsocketRouteHandler(path)(self._ws_handler_func)]
route_handlers = [WebsocketRouteHandler(path, fn=self._ws_handler_func)]
else:
route_handlers = [
WebsocketRouteHandler(self._handler_root_path + channel_name)(
self._create_ws_handler_func(channel_name)
WebsocketRouteHandler(
self._handler_root_path + channel_name, fn=self._create_ws_handler_func(channel_name)
)
for channel_name in self._channels
]
Expand Down
2 changes: 1 addition & 1 deletion litestar/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def get_route_handlers(self) -> list[BaseRouteHandler]:
route_handler = deepcopy(self_handler)
# at the point we get a reference to the handler function, it's unbound, so
# we replace it with a regular bound method here
route_handler._fn = types.MethodType(route_handler._fn, self)
route_handler.fn = types.MethodType(route_handler.fn, self)
route_handler.owner = self
route_handlers.append(route_handler)

Expand Down
68 changes: 57 additions & 11 deletions litestar/handlers/asgi_handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence

from litestar.exceptions import ImproperlyConfiguredException
from litestar.handlers.base import BaseRouteHandler
Expand All @@ -13,24 +13,20 @@
if TYPE_CHECKING:
from litestar.connection import ASGIConnection
from litestar.types import (
AsyncAnyCallable,
ExceptionHandlersMap,
Guard,
MaybePartial, # noqa: F401
)


class ASGIRouteHandler(BaseRouteHandler):
"""ASGI Route Handler decorator.

Use this decorator to decorate ASGI applications.
"""

__slots__ = ("is_mount",)

def __init__(
self,
path: str | Sequence[str] | None = None,
*,
fn: AsyncAnyCallable,
exception_handlers: ExceptionHandlersMap | None = None,
guards: Sequence[Guard] | None = None,
name: str | None = None,
Expand All @@ -39,17 +35,20 @@ def __init__(
signature_namespace: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize ``ASGIRouteHandler``.
"""Route handler for ASGI routes.

Args:
path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
``/``.
fn: The handler function.

.. versionadded:: 3.0
exception_handlers: A mapping of status codes and/or exception types to handler functions.
guards: A sequence of :class:`Guard <.types.Guard>` callables.
name: A string identifying the route handler.
opt: A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
wherever you have access to :class:`Request <.connection.Request>` or
:class:`ASGI Scope <.types.Scope>`.
path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
``/``
is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
Expand All @@ -61,6 +60,7 @@ def __init__(
self.is_mount = is_mount
super().__init__(
path,
fn=fn,
exception_handlers=exception_handlers,
guards=guards,
name=name,
Expand Down Expand Up @@ -101,4 +101,50 @@ async def handle(self, connection: ASGIConnection[ASGIRouteHandler, Any, Any, An
await self.fn(scope=connection.scope, receive=connection.receive, send=connection.send)


asgi = ASGIRouteHandler
def asgi(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this public/documented?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the replacement for v2's asgi type. Needs a docstring?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and a .. versionadded:: directive :D

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm not so sure about the versionadded with these. The names aren't newly introduced, and their usage remains largely the same. The fact that they're now implemented as decorator functions instead of callable class instance is an implementation detail. I think it's fair to say this is merely a change and not addition of a new feature.

wdyt @JacobCoffee @peterschutt?

path: str | Sequence[str] | None = None,
*,
exception_handlers: ExceptionHandlersMap | None = None,
guards: Sequence[Guard] | None = None,
name: str | None = None,
opt: Mapping[str, Any] | None = None,
is_mount: bool = False,
signature_namespace: Mapping[str, Any] | None = None,
handler_class: type[ASGIRouteHandler] = ASGIRouteHandler,
**kwargs: Any,
) -> Callable[[AsyncAnyCallable], ASGIRouteHandler]:
"""Create an :class:`ASGIRouteHandler`.

Args:
path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
to ``/``
exception_handlers: A mapping of status codes and/or exception types to handler functions.
guards: A sequence of :class:`Guard <.types.Guard>` callables.
name: A string identifying the route handler.
opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
wherever you have access to :class:`Request <.connection.Request>` or
:class:`ASGI Scope <.types.Scope>`.
signature_namespace: A mapping of names to types for use in forward reference resolution during signature
modelling.
is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
``/some-path/sub-path/`` etc.
handler_class: Route handler class instantiated by the decorator
**kwargs: Any additional kwarg - will be set in the opt dictionary.
"""

def decorator(fn: AsyncAnyCallable) -> ASGIRouteHandler:
return handler_class(
fn=fn,
path=path,
exception_handlers=exception_handlers,
guards=guards,
name=name,
opt=opt,
is_mount=is_mount,
signature_namespace=signature_namespace,
**kwargs,
)

return decorator
Loading
Loading