Skip to content

Commit 4dc78a8

Browse files
authored
feat!: Make route handlers functional decorators (#3436)
* make route handlers functional decorators
1 parent 8aae5d8 commit 4dc78a8

37 files changed

+1443
-1019
lines changed

docs/release-notes/2.x-changelog.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -3057,7 +3057,7 @@
30573057
:pr: 1647
30583058

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

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

32143214
.. change:: Add ``connection_accept_handler`` to ``websocket_listener``
@@ -3217,7 +3217,7 @@
32173217
:issue: 1571
32183218

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

@@ -3305,7 +3305,7 @@
33053305
appropriate event hooks - to use a context manager.
33063306

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

@@ -3419,7 +3419,7 @@
34193419
:pr: 1518
34203420

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

34253425
.. change:: DTO based serialization plugin

docs/release-notes/whats-new-3.rst

+36
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,39 @@ If you were relying on this utility, you can define it yourself as follows:
142142
143143
def is_sync_or_async_generator(obj: Any) -> bool:
144144
return isgeneratorfunction(obj) or isasyncgenfunction(obj)
145+
146+
147+
Removal of semantic HTTP route handler classes
148+
-----------------------------------------------
149+
150+
The semantic ``HTTPRouteHandler`` classes have been removed in favour of functional
151+
decorators. ``route``, ``get``, ``post``, ``patch``, ``put``, ``head`` and ``delete``
152+
are now all decorator functions returning :class:`~.handlers.HTTPRouteHandler`
153+
instances.
154+
155+
As a result, customizing the decorators directly is not possible anymore. Instead, to
156+
use a route handler decorator with a custom route handler class, the ``handler_class``
157+
parameter to the decorator function can be used:
158+
159+
Before:
160+
161+
.. code-block:: python
162+
163+
class my_get_handler(get):
164+
... # custom handler
165+
166+
@my_get_handler()
167+
async def handler() -> Any:
168+
...
169+
170+
After:
171+
172+
.. code-block:: python
173+
174+
class MyHTTPRouteHandler(HTTPRouteHandler):
175+
... # custom handler
176+
177+
178+
@get(handler_class=MyHTTPRouteHandler)
179+
async def handler() -> Any:
180+
...

docs/usage/routing/handlers.rst

+28-26
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ handler :term:`decorators <decorator>` exported from Litestar.
77
For example:
88

99
.. code-block:: python
10-
:caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator`
10+
:caption: Defining a route handler by decorating a function with the :func:`@get() <.handlers.get>` :term:`decorator`
1111
1212
from litestar import get
1313
@@ -146,12 +146,11 @@ There are several reasons for why this limitation is enforced:
146146
HTTP route handlers
147147
-------------------
148148
149-
The most commonly used route handlers are those that handle HTTP requests and responses.
150-
These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the
151-
:term:`decorator` called :func:`~.handlers.route`:
149+
The :class:`~.handlers.HTTPRouteHandler` is used to handle HTTP requests, and can be
150+
created with the :func:`~.handlers.route` :term:`decorator`:
152151
153152
.. code-block:: python
154-
:caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>`
153+
:caption: Defining a route handler by decorating a function with the :func:`@route() <.handlers.route>`
155154
:term:`decorator`
156155
157156
from litestar import HttpMethod, route
@@ -160,20 +159,24 @@ These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` c
160159
@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
161160
async def my_endpoint() -> None: ...
162161
163-
As mentioned above, :func:`@route() <.handlers.route>` is merely an alias for ``HTTPRouteHandler``,
164-
thus the below code is equivalent to the one above:
162+
The same can be achieved without a decorator, by using ``HTTPRouteHandler`` directly:
165163
166164
.. code-block:: python
167-
:caption: Defining a route handler by decorating a function with the
168-
:class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class
165+
:caption: Defining a route handler creating an instance of
166+
:class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>`
169167
170168
from litestar import HttpMethod
171169
from litestar.handlers.http_handlers import HTTPRouteHandler
172170
173171
174-
@HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
175172
async def my_endpoint() -> None: ...
176173
174+
handler = HTTPRouteHandler(
175+
path="/some-path",
176+
http_method=[HttpMethod.GET, HttpMethod.POST],
177+
fn=my_endpoint
178+
)
179+
177180
178181
Semantic handler :term:`decorators <decorator>`
179182
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -189,8 +192,8 @@ which correlates with their name:
189192
* :func:`@post() <.handlers.post>`
190193
* :func:`@put() <.handlers.put>`
191194
192-
These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you cannot configure the
193-
:paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg <argument>`:
195+
These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you don't need to configure
196+
the :paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg <argument>`:
194197
195198
.. dropdown:: Click to see the predefined route handlers
196199
@@ -240,11 +243,6 @@ These are used exactly like :func:`@route() <.handlers.route>` with the sole exc
240243
@delete(path="/resources/{pk:int}")
241244
async def delete_resource(pk: int) -> None: ...
242245
243-
Although these :term:`decorators <decorator>` are merely subclasses of :class:`~.handlers.HTTPRouteHandler` that pre-set
244-
the :paramref:`~.handlers.HTTPRouteHandler.http_method`, using :func:`@get() <.handlers.get>`,
245-
:func:`@patch() <.handlers.patch>`, :func:`@put() <.handlers.put>`, :func:`@delete() <.handlers.delete>`, or
246-
:func:`@post() <.handlers.post>` instead of :func:`@route() <.handlers.route>` makes the code clearer and simpler.
247-
248246
Furthermore, in the OpenAPI specification each unique combination of HTTP verb (e.g. ``GET``, ``POST``, etc.) and path
249247
is regarded as a distinct `operation <https://spec.openapis.org/oas/latest.html#operation-object>`_\ , and each
250248
operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally
@@ -277,22 +275,25 @@ A WebSocket connection can be handled with a :func:`@websocket() <.handlers.Webs
277275
await socket.send_json({...})
278276
await socket.close()
279277
280-
The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is an alias of the
281-
:class:`~.handlers.WebsocketRouteHandler` class. Thus, the below code is equivalent to the one above:
278+
The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` can be used to create an instance of
279+
:class:`~.handlers.WebsocketRouteHandler`. Therefore, the below code is equivalent to the one above:
282280
283281
.. code-block:: python
284282
:caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly
285283
286284
from litestar import WebSocket
287285
from litestar.handlers.websocket_handlers import WebsocketRouteHandler
288286
289-
290-
@WebsocketRouteHandler(path="/socket")
291287
async def my_websocket_handler(socket: WebSocket) -> None:
292288
await socket.accept()
293289
await socket.send_json({...})
294290
await socket.close()
295291
292+
my_websocket_handler = WebsocketRouteHandler(
293+
path="/socket",
294+
fn=my_websocket_handler,
295+
)
296+
296297
In difference to HTTP routes handlers, websocket handlers have the following requirements:
297298
298299
#. They **must** declare a ``socket`` :term:`kwarg <argument>`.
@@ -332,8 +333,8 @@ If you need to write your own ASGI application, you can do so using the :func:`@
332333
)
333334
await response(scope=scope, receive=receive, send=send)
334335
335-
Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator` is an alias of the
336-
:class:`~.handlers.ASGIRouteHandler` class. Thus, the code below is equivalent to the one above:
336+
:func:`@asgi() <.handlers.asgi>` :term:`decorator` can be used to create an instance of
337+
:class:`~.handlers.ASGIRouteHandler`. Therefore, the code below is equivalent to the one above:
337338
338339
.. code-block:: python
339340
:caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly
@@ -343,8 +344,6 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
343344
from litestar.status_codes import HTTP_400_BAD_REQUEST
344345
from litestar.types import Scope, Receive, Send
345346
346-
347-
@ASGIRouteHandler(path="/my-asgi-app")
348347
async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
349348
if scope["type"] == "http":
350349
if scope["method"] == "GET":
@@ -356,7 +355,10 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
356355
)
357356
await response(scope=scope, receive=receive, send=send)
358357
359-
Limitations of ASGI route handlers
358+
my_asgi_app = ASGIRouteHandler(path="/my-asgi-app", fn=my_asgi_app)
359+
360+
361+
ASGI route handler considerations
360362
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
361363
362364
In difference to the other route handlers, the :func:`@asgi() <.handlers.asgi>` route handler accepts only three

docs/usage/websockets.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ exceptions, and parsing incoming and serializing outgoing data. In addition to t
88
low-level :class:`WebSocket route handler <.handlers.websocket>`, Litestar offers two
99
high level interfaces:
1010

11-
- :class:`websocket_listener <.handlers.websocket_listener>`
11+
- :func:`websocket_listener <.handlers.websocket_listener>`
1212
- :class:`WebSocketListener <.handlers.WebsocketListener>`
1313

1414

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

3939
.. note::
4040
Contrary to WebSocket route handlers, functions decorated with
41-
:class:`websocket_listener <.handlers.websocket_listener>` don't have to be
41+
:func:`websocket_listener <.handlers.websocket_listener>` don't have to be
4242
asynchronous.
4343

4444

litestar/channels/plugin.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig:
116116
if self._create_route_handlers:
117117
if self._arbitrary_channels_allowed:
118118
path = self._handler_root_path + "{channel_name:str}"
119-
route_handlers = [WebsocketRouteHandler(path)(self._ws_handler_func)]
119+
route_handlers = [WebsocketRouteHandler(path, fn=self._ws_handler_func)]
120120
else:
121121
route_handlers = [
122-
WebsocketRouteHandler(self._handler_root_path + channel_name)(
123-
self._create_ws_handler_func(channel_name)
122+
WebsocketRouteHandler(
123+
self._handler_root_path + channel_name, fn=self._create_ws_handler_func(channel_name)
124124
)
125125
for channel_name in self._channels
126126
]

litestar/controller.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def get_route_handlers(self) -> list[BaseRouteHandler]:
222222
route_handler = deepcopy(self_handler)
223223
# at the point we get a reference to the handler function, it's unbound, so
224224
# we replace it with a regular bound method here
225-
route_handler._fn = types.MethodType(route_handler._fn, self)
225+
route_handler.fn = types.MethodType(route_handler.fn, self)
226226
route_handler.owner = self
227227
route_handlers.append(route_handler)
228228

litestar/handlers/asgi_handlers.py

+57-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Mapping, Sequence
3+
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence
44

55
from litestar.exceptions import ImproperlyConfiguredException
66
from litestar.handlers.base import BaseRouteHandler
@@ -13,24 +13,20 @@
1313
if TYPE_CHECKING:
1414
from litestar.connection import ASGIConnection
1515
from litestar.types import (
16+
AsyncAnyCallable,
1617
ExceptionHandlersMap,
1718
Guard,
18-
MaybePartial, # noqa: F401
1919
)
2020

2121

2222
class ASGIRouteHandler(BaseRouteHandler):
23-
"""ASGI Route Handler decorator.
24-
25-
Use this decorator to decorate ASGI applications.
26-
"""
27-
2823
__slots__ = ("is_mount",)
2924

3025
def __init__(
3126
self,
3227
path: str | Sequence[str] | None = None,
3328
*,
29+
fn: AsyncAnyCallable,
3430
exception_handlers: ExceptionHandlersMap | None = None,
3531
guards: Sequence[Guard] | None = None,
3632
name: str | None = None,
@@ -39,17 +35,20 @@ def __init__(
3935
signature_namespace: Mapping[str, Any] | None = None,
4036
**kwargs: Any,
4137
) -> None:
42-
"""Initialize ``ASGIRouteHandler``.
38+
"""Route handler for ASGI routes.
4339
4440
Args:
41+
path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
42+
``/``.
43+
fn: The handler function.
44+
45+
.. versionadded:: 3.0
4546
exception_handlers: A mapping of status codes and/or exception types to handler functions.
4647
guards: A sequence of :class:`Guard <.types.Guard>` callables.
4748
name: A string identifying the route handler.
4849
opt: A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
4950
wherever you have access to :class:`Request <.connection.Request>` or
5051
:class:`ASGI Scope <.types.Scope>`.
51-
path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
52-
``/``
5352
is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
5453
accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
5554
``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
@@ -61,6 +60,7 @@ def __init__(
6160
self.is_mount = is_mount
6261
super().__init__(
6362
path,
63+
fn=fn,
6464
exception_handlers=exception_handlers,
6565
guards=guards,
6666
name=name,
@@ -101,4 +101,50 @@ async def handle(self, connection: ASGIConnection[ASGIRouteHandler, Any, Any, An
101101
await self.fn(scope=connection.scope, receive=connection.receive, send=connection.send)
102102

103103

104-
asgi = ASGIRouteHandler
104+
def asgi(
105+
path: str | Sequence[str] | None = None,
106+
*,
107+
exception_handlers: ExceptionHandlersMap | None = None,
108+
guards: Sequence[Guard] | None = None,
109+
name: str | None = None,
110+
opt: Mapping[str, Any] | None = None,
111+
is_mount: bool = False,
112+
signature_namespace: Mapping[str, Any] | None = None,
113+
handler_class: type[ASGIRouteHandler] = ASGIRouteHandler,
114+
**kwargs: Any,
115+
) -> Callable[[AsyncAnyCallable], ASGIRouteHandler]:
116+
"""Create an :class:`ASGIRouteHandler`.
117+
118+
Args:
119+
path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
120+
to ``/``
121+
exception_handlers: A mapping of status codes and/or exception types to handler functions.
122+
guards: A sequence of :class:`Guard <.types.Guard>` callables.
123+
name: A string identifying the route handler.
124+
opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
125+
wherever you have access to :class:`Request <.connection.Request>` or
126+
:class:`ASGI Scope <.types.Scope>`.
127+
signature_namespace: A mapping of names to types for use in forward reference resolution during signature
128+
modelling.
129+
is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
130+
accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
131+
``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
132+
``/some-path/sub-path/`` etc.
133+
handler_class: Route handler class instantiated by the decorator
134+
**kwargs: Any additional kwarg - will be set in the opt dictionary.
135+
"""
136+
137+
def decorator(fn: AsyncAnyCallable) -> ASGIRouteHandler:
138+
return handler_class(
139+
fn=fn,
140+
path=path,
141+
exception_handlers=exception_handlers,
142+
guards=guards,
143+
name=name,
144+
opt=opt,
145+
is_mount=is_mount,
146+
signature_namespace=signature_namespace,
147+
**kwargs,
148+
)
149+
150+
return decorator

0 commit comments

Comments
 (0)