From 4dc78a8298e1ddf1551d0e3a02d4278a9c276801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sat, 25 May 2024 12:05:39 +0200 Subject: [PATCH] feat!: Make route handlers functional decorators (#3436) * make route handlers functional decorators --- docs/release-notes/2.x-changelog.rst | 10 +- docs/release-notes/whats-new-3.rst | 36 + docs/usage/routing/handlers.rst | 54 +- docs/usage/websockets.rst | 4 +- litestar/channels/plugin.py | 6 +- litestar/controller.py | 2 +- litestar/handlers/asgi_handlers.py | 68 +- litestar/handlers/base.py | 31 +- litestar/handlers/http_handlers/__init__.py | 4 +- litestar/handlers/http_handlers/_options.py | 7 +- litestar/handlers/http_handlers/base.py | 54 +- litestar/handlers/http_handlers/decorators.py | 1687 +++++++++-------- .../handlers/websocket_handlers/listener.py | 158 +- .../websocket_handlers/route_handler.py | 68 +- litestar/testing/request_factory.py | 4 +- litestar/types/__init__.py | 2 + litestar/types/callable_types.py | 1 + tests/e2e/test_router_registration.py | 4 +- .../e2e/test_routing/test_path_resolution.py | 34 +- tests/e2e/test_routing/test_route_indexing.py | 22 +- tests/e2e/test_routing/test_route_reverse.py | 15 +- tests/unit/test_controller.py | 7 +- .../test_asgi_handlers/test_handle_asgi.py | 12 + .../test_base_handlers/test_opt.py | 8 +- .../test_base_handlers/test_validations.py | 8 - .../test_custom_handler_class.py | 30 + .../test_http_handlers/test_defaults.py | 2 +- .../test_http_handlers/test_head.py | 12 +- .../test_http_handlers/test_kwarg_handling.py | 30 +- .../test_signature_namespace.py | 14 +- .../test_http_handlers/test_validations.py | 8 +- .../test_custom_handler_class.py | 12 + .../test_websocket_handlers/test_listeners.py | 13 +- tests/unit/test_kwargs/test_validations.py | 6 +- .../test_rate_limit_middleware.py | 2 +- tests/unit/test_openapi/test_path_item.py | 25 +- tests/unit/test_signature/test_validation.py | 2 +- 37 files changed, 1443 insertions(+), 1019 deletions(-) create mode 100644 tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py create mode 100644 tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py diff --git a/docs/release-notes/2.x-changelog.rst b/docs/release-notes/2.x-changelog.rst index a9792f931e..3bd45b7a6c 100644 --- a/docs/release-notes/2.x-changelog.rst +++ b/docs/release-notes/2.x-changelog.rst @@ -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. @@ -3208,7 +3208,7 @@ :issue: 1615 A bug was fixed that would cause a type error when using a - :class:`websocket_listener ` + :func:`websocket_listener ` in a ``Controller`` .. change:: Add ``connection_accept_handler`` to ``websocket_listener`` @@ -3217,7 +3217,7 @@ :issue: 1571 Add a new ``connection_accept_handler`` parameter to - :class:`websocket_listener `, + :func:`websocket_listener `, which can be used to customize how a connection is accepted, for example to add headers or subprotocols @@ -3305,7 +3305,7 @@ appropriate event hooks - to use a context manager. The ``connection_lifespan`` argument was added to the - :class:`WebSocketListener `, which accepts + :func:`WebSocketListener `, which accepts an asynchronous context manager, which can be used to handle the lifespan of the socket. @@ -3419,7 +3419,7 @@ :pr: 1518 Support for DTOs has been added to :class:`WebSocketListener ` and - :class:`WebSocketListener `. A ``dto`` and ``return_dto`` parameter has + :func:`WebSocketListener `. A ``dto`` and ``return_dto`` parameter has been added, providing the same functionality as their route handler counterparts. .. change:: DTO based serialization plugin diff --git a/docs/release-notes/whats-new-3.rst b/docs/release-notes/whats-new-3.rst index e910c8eb63..a34d6df294 100644 --- a/docs/release-notes/whats-new-3.rst +++ b/docs/release-notes/whats-new-3.rst @@ -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. + +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: + ... diff --git a/docs/usage/routing/handlers.rst b/docs/usage/routing/handlers.rst index 78fc9f6e7d..289ef81e7b 100644 --- a/docs/usage/routing/handlers.rst +++ b/docs/usage/routing/handlers.rst @@ -7,7 +7,7 @@ handler :term:`decorators ` 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 @@ -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 @@ -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 ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -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 `: +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 `: .. dropdown:: Click to see the predefined route handlers @@ -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 ` 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 `_\ , and each operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally @@ -277,8 +275,8 @@ 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 @@ -286,13 +284,16 @@ The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is 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 `. @@ -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 @@ -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": @@ -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 diff --git a/docs/usage/websockets.rst b/docs/usage/websockets.rst index dad724587b..9aa2c8ebd9 100644 --- a/docs/usage/websockets.rst +++ b/docs/usage/websockets.rst @@ -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>` @@ -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. diff --git a/litestar/channels/plugin.py b/litestar/channels/plugin.py index 59884454d4..5bdac7f9c5 100644 --- a/litestar/channels/plugin.py +++ b/litestar/channels/plugin.py @@ -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 ] diff --git a/litestar/controller.py b/litestar/controller.py index 967454b168..bd7e1a604b 100644 --- a/litestar/controller.py +++ b/litestar/controller.py @@ -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) diff --git a/litestar/handlers/asgi_handlers.py b/litestar/handlers/asgi_handlers.py index bcf220a0cc..03f0507308 100644 --- a/litestar/handlers/asgi_handlers.py +++ b/litestar/handlers/asgi_handlers.py @@ -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 @@ -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, @@ -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. @@ -61,6 +60,7 @@ def __init__( self.is_mount = is_mount super().__init__( path, + fn=fn, exception_handlers=exception_handlers, guards=guards, name=name, @@ -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( + 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 diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py index c4932ecdb6..c3b74ab201 100644 --- a/litestar/handlers/base.py +++ b/litestar/handlers/base.py @@ -48,7 +48,6 @@ class BaseRouteHandler: """ __slots__ = ( - "_fn", "_parsed_data_field", "_parsed_fn_signature", "_parsed_return_field", @@ -64,6 +63,7 @@ class BaseRouteHandler: "dependencies", "dto", "exception_handlers", + "fn", "guards", "middleware", "name", @@ -80,6 +80,7 @@ def __init__( self, path: str | Sequence[str] | None = None, *, + fn: AsyncAnyCallable, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, exception_handlers: ExceptionHandlersMap | None = None, @@ -99,6 +100,9 @@ def __init__( Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` + fn: The handler function + + .. versionadded:: 3.0 dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. @@ -151,11 +155,10 @@ def __init__( self.paths = ( {normalize_path(p) for p in path} if path and isinstance(path, list) else {normalize_path(path or "/")} # type: ignore[arg-type] ) + self.fn = self._prepare_fn(fn) - def __call__(self, fn: AsyncAnyCallable) -> Self: - """Replace a function with itself.""" - self._fn = fn - return self + def _prepare_fn(self, fn: AsyncAnyCallable) -> AsyncAnyCallable: + return fn @property def handler_id(self) -> str: @@ -200,20 +203,6 @@ def signature_model(self) -> type[SignatureModel]: ) return self._signature_model - @property - def fn(self) -> AsyncAnyCallable: - """Get the handler function. - - Raises: - ImproperlyConfiguredException: if handler fn is not set. - - Returns: - Handler function - """ - if not hasattr(self, "_fn"): - raise ImproperlyConfiguredException("No callable has been registered for this handler") - return self._fn - @property def parsed_fn_signature(self) -> ParsedSignature: """Return the parsed signature of the handler function. @@ -434,13 +423,13 @@ def resolve_signature_namespace(self) -> dict[str, Any]: When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the layer closest to the response handler will take precedence. """ - if self._resolved_layered_parameters is Empty: + if self._resolved_signature_namespace is Empty: ns: dict[str, Any] = {} for layer in self.ownership_layers: ns.update(layer.signature_namespace) self._resolved_signature_namespace = ns - return cast("dict[str, Any]", self._resolved_signature_namespace) + return self._resolved_signature_namespace def resolve_data_dto(self) -> type[AbstractDTO] | None: """Resolve the data_dto by starting from the route handler and moving up. diff --git a/litestar/handlers/http_handlers/__init__.py b/litestar/handlers/http_handlers/__init__.py index 844f046895..3009fda95b 100644 --- a/litestar/handlers/http_handlers/__init__.py +++ b/litestar/handlers/http_handlers/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations -from .base import HTTPRouteHandler, route -from .decorators import delete, get, head, patch, post, put +from .base import HTTPRouteHandler +from .decorators import delete, get, head, patch, post, put, route __all__ = ( "HTTPRouteHandler", diff --git a/litestar/handlers/http_handlers/_options.py b/litestar/handlers/http_handlers/_options.py index b46a3740b3..fb6a409114 100644 --- a/litestar/handlers/http_handlers/_options.py +++ b/litestar/handlers/http_handlers/_options.py @@ -33,8 +33,5 @@ def options_handler() -> Response: ) return HTTPRouteHandler( - path=path, - http_method=[HttpMethod.OPTIONS], - include_in_schema=False, - sync_to_thread=False, - )(options_handler) + path=path, http_method=[HttpMethod.OPTIONS], include_in_schema=False, sync_to_thread=False, fn=options_handler + ) diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py index dcb11cd58a..1cc9227cfa 100644 --- a/litestar/handlers/http_handlers/base.py +++ b/litestar/handlers/http_handlers/base.py @@ -27,7 +27,8 @@ normalize_http_method, ) from litestar.openapi.spec import Operation -from litestar.response import Response +from litestar.response import File, Response +from litestar.response.file import ASGIFileResponse from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED from litestar.types import ( AfterRequestHookHandler, @@ -50,6 +51,7 @@ Send, TypeEncodersMap, ) +from litestar.types.builtin_types import NoneType from litestar.utils import ensure_async_callable from litestar.utils.predicates import is_async_callable from litestar.utils.scope.state import ScopeState @@ -71,7 +73,7 @@ from litestar.types.callable_types import AsyncAnyCallable, OperationIDCreator from litestar.types.composite_types import TypeDecodersSequence -__all__ = ("HTTPRouteHandler", "route") +__all__ = ("HTTPRouteHandler",) class ResponseHandlerMap(TypedDict): @@ -80,11 +82,6 @@ class ResponseHandlerMap(TypedDict): class HTTPRouteHandler(BaseRouteHandler): - """HTTP Route Decorator. - - Use this decorator to decorate an HTTP handler with multiple methods. - """ - __slots__ = ( "_resolved_after_response", "_resolved_before_request", @@ -128,12 +125,11 @@ class HTTPRouteHandler(BaseRouteHandler): "template_name", ) - has_sync_callable: bool - def __init__( self, path: str | Sequence[str] | None = None, *, + fn: AnyCallable, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, @@ -177,11 +173,12 @@ def __init__( type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: - """Initialize ``HTTPRouteHandler``. + """Route handler for HTTP routes. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` + fn: The handler function after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. @@ -255,7 +252,18 @@ def __init__( self.http_methods = normalize_http_method(http_methods=http_method) self.status_code = status_code or get_default_status_code(http_methods=self.http_methods) + if has_sync_callable := not is_async_callable(fn): + if sync_to_thread is None: + warn_implicit_sync_to_thread(fn, stacklevel=3) + elif sync_to_thread is not None: + warn_sync_to_thread_with_async_callable(fn, stacklevel=3) + + if has_sync_callable and sync_to_thread: + fn = ensure_async_callable(fn) + has_sync_callable = False + super().__init__( + fn=fn, path=path, dependencies=dependencies, dto=dto, @@ -285,7 +293,7 @@ def __init__( self.response_cookies: Sequence[Cookie] | None = narrow_response_cookies(response_cookies) self.response_headers: Sequence[ResponseHeader] | None = narrow_response_headers(response_headers) - self.sync_to_thread = sync_to_thread + self.has_sync_callable = has_sync_callable # OpenAPI related attributes self.content_encoding = content_encoding self.content_media_type = content_media_type @@ -311,17 +319,6 @@ def __init__( self._resolved_tags: list[str] | EmptyType = Empty self._kwargs_models: dict[tuple[str, ...], KwargsModel] = {} - def __call__(self, fn: AnyCallable) -> HTTPRouteHandler: - """Replace a function with itself.""" - if not is_async_callable(fn): - if self.sync_to_thread is None: - warn_implicit_sync_to_thread(fn, stacklevel=3) - elif self.sync_to_thread is not None: - warn_sync_to_thread_with_async_callable(fn, stacklevel=3) - - super().__call__(fn) - return self - def resolve_request_class(self) -> type[Request]: """Return the closest custom Request class in the owner graph or the default Request class. @@ -573,11 +570,6 @@ def on_registration(self, app: Litestar, route: BaseRoute) -> None: super().on_registration(app, route=route) self.resolve_after_response() self.resolve_include_in_schema() - self.has_sync_callable = not is_async_callable(self.fn) - - if self.has_sync_callable and self.sync_to_thread: - self._fn = ensure_async_callable(self.fn) - self.has_sync_callable = False self._get_kwargs_model_for_route(route.path_parameters) @@ -619,6 +611,11 @@ def _validate_handler_function(self) -> None: if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods: raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers") + if self.http_methods == {HttpMethod.HEAD} and not self.parsed_fn_signature.return_type.is_subclass_of( + (NoneType, File, ASGIFileResponse) + ): + raise ImproperlyConfiguredException("A response to a head request should not have a body") + async def handle(self, connection: Request[Any, Any, Any]) -> None: """ASGI app that creates a :class:`~.connection.Request` from the passed in args, determines which handler function to call and then handles the call. @@ -754,6 +751,3 @@ async def cached_response(scope: Scope, receive: Receive, send: Send) -> None: await send(message) return cached_response - - -route = HTTPRouteHandler diff --git a/litestar/handlers/http_handlers/decorators.py b/litestar/handlers/http_handlers/decorators.py index 1ae72e559b..02e304713a 100644 --- a/litestar/handlers/http_handlers/decorators.py +++ b/litestar/handlers/http_handlers/decorators.py @@ -1,16 +1,29 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence from litestar.enums import HttpMethod, MediaType -from litestar.exceptions import HTTPException, ImproperlyConfiguredException -from litestar.openapi.spec import Operation -from litestar.response.file import ASGIFileResponse, File -from litestar.types import Empty, TypeDecodersSequence -from litestar.types.builtin_types import NoneType -from litestar.utils import is_class_and_subclass - -from .base import HTTPRouteHandler +from litestar.handlers.http_handlers.base import HTTPRouteHandler +from litestar.openapi.spec import Operation, SecurityRequirement +from litestar.types import ( + AfterRequestHookHandler, + AfterResponseHookHandler, + AnyCallable, + BeforeRequestHookHandler, + CacheKeyBuilder, + Dependencies, + Empty, + EmptyType, + ExceptionHandlersMap, + Guard, + Method, + Middleware, + OperationIDCreator, + ResponseCookies, + ResponseHeaders, + TypeDecodersSequence, + TypeEncodersMap, +) if TYPE_CHECKING: from typing import Any, Mapping, Sequence @@ -20,6 +33,7 @@ from litestar.connection import Request from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO + from litestar.exceptions import HTTPException from litestar.openapi.datastructures import ResponseSpec from litestar.openapi.spec import SecurityRequirement from litestar.response import Response @@ -37,141 +51,137 @@ ResponseHeaders, TypeEncodersMap, ) - from litestar.types.callable_types import OperationIDCreator - + from litestar.types.callable_types import AnyCallable, OperationIDCreator -__all__ = ("get", "head", "post", "put", "patch", "delete") +__all__ = ("get", "head", "post", "put", "patch", "delete", "route") -MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP = "semantic route handlers cannot define http_method" +def route( + path: str | None | Sequence[str] = None, + *, + http_method: HttpMethod | Method | Sequence[HttpMethod | Method], + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler`. -class delete(HTTPRouteHandler): - """DELETE Route Decorator. + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod ` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - Use this decorator to decorate an HTTP handler for DELETE requests. + **kwargs: Any additional kwarg - will be set in the opt dictionary. """ - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``delete`` - - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` - and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) - super().__init__( + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, + http_method=http_method, after_request=after_request, after_response=after_response, background=background, @@ -188,7 +198,6 @@ def __init__( etag=etag, exception_handlers=exception_handlers, guards=guards, - http_method=HttpMethod.DELETE, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, @@ -216,135 +225,130 @@ def __init__( **kwargs, ) + return decorator -class get(HTTPRouteHandler): - """GET Route Decorator. - Use this decorator to decorate an HTTP handler for GET requests. - """ +def get( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``GET`` method. - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``get``. + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ - super().__init__( + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, after_request=after_request, after_response=after_response, background=background, @@ -389,139 +393,134 @@ def __init__( **kwargs, ) + return decorator -class head(HTTPRouteHandler): - """HEAD Route Decorator. - Use this decorator to decorate an HTTP handler for HEAD requests. - """ +def head( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``HEAD`` method. - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``head``. + Notes: + - A response to a head request cannot include a body. + See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD). - Notes: - - A response to a head request cannot include a body. - See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD). + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ - super().__init__( + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, after_request=after_request, after_response=after_response, background=background, @@ -566,147 +565,130 @@ def __init__( **kwargs, ) - def _validate_handler_function(self) -> None: - """Validate the route handler function once it is set by inspecting its return annotations.""" - super()._validate_handler_function() + return decorator - # we allow here File and File because these have special setting for head responses - return_annotation = self.parsed_fn_signature.return_type.annotation - if not ( - return_annotation in {NoneType, None} - or is_class_and_subclass(return_annotation, File) - or is_class_and_subclass(return_annotation, ASGIFileResponse) - ): - raise ImproperlyConfiguredException("A response to a head request should not have a body") +def patch( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``PATCH`` method. -class patch(HTTPRouteHandler): - """PATCH Route Decorator. + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - Use this decorator to decorate an HTTP handler for PATCH requests. + **kwargs: Any additional kwarg - will be set in the opt dictionary. """ - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``patch``. - - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) - super().__init__( + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, after_request=after_request, after_response=after_response, background=background, @@ -751,134 +733,130 @@ def __init__( **kwargs, ) + return decorator -class post(HTTPRouteHandler): - """POST Route Decorator. - Use this decorator to decorate an HTTP handler for POST requests. - """ +def post( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``POST`` method. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``post`` + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) - super().__init__( + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, after_request=after_request, after_response=after_response, background=background, @@ -923,134 +901,130 @@ def __init__( **kwargs, ) + return decorator -class put(HTTPRouteHandler): - """PUT Route Decorator. - Use this decorator to decorate an HTTP handler for PUT requests. - """ +def put( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``PUT`` method. - def __init__( - self, - path: str | None | Sequence[str] = None, - *, - after_request: AfterRequestHookHandler | None = None, - after_response: AfterResponseHookHandler | None = None, - background: BackgroundTask | BackgroundTasks | None = None, - before_request: BeforeRequestHookHandler | None = None, - cache: bool | int | type[CACHE_FOREVER] = False, - cache_control: CacheControlHeader | None = None, - cache_key_builder: CacheKeyBuilder | None = None, - dependencies: Dependencies | None = None, - dto: type[AbstractDTO] | None | EmptyType = Empty, - etag: ETag | None = None, - exception_handlers: ExceptionHandlersMap | None = None, - guards: Sequence[Guard] | None = None, - media_type: MediaType | str | None = None, - middleware: Sequence[Middleware] | None = None, - name: str | None = None, - opt: Mapping[str, Any] | None = None, - request_class: type[Request] | None = None, - response_class: type[Response] | None = None, - response_cookies: ResponseCookies | None = None, - response_headers: ResponseHeaders | None = None, - return_dto: type[AbstractDTO] | None | EmptyType = Empty, - signature_namespace: Mapping[str, Any] | None = None, - status_code: int | None = None, - sync_to_thread: bool | None = None, - # OpenAPI related attributes - content_encoding: str | None = None, - content_media_type: str | None = None, - deprecated: bool = False, - description: str | None = None, - include_in_schema: bool | EmptyType = Empty, - operation_class: type[Operation] = Operation, - operation_id: str | OperationIDCreator | None = None, - raises: Sequence[type[HTTPException]] | None = None, - response_description: str | None = None, - responses: Mapping[int, ResponseSpec] | None = None, - security: Sequence[SecurityRequirement] | None = None, - summary: str | None = None, - tags: Sequence[str] | None = None, - type_decoders: TypeDecodersSequence | None = None, - type_encoders: TypeEncodersMap | None = None, - **kwargs: Any, - ) -> None: - """Initialize ``put`` + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator - Args: - path: A path fragment for the route handler function or a sequence of path fragments. - If not given defaults to ``/`` - after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed - to any route handler. If this function returns a value, the request will not reach the route handler, - and instead this value will be used. - after_response: A sync or async function called after the response has been awaited. It receives the - :class:`Request <.connection.Request>` object and should not return any values. - background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or - :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. - Defaults to ``None``. - before_request: A sync or async function called immediately before calling the route handler. Receives - the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, - bypassing the route handler. - cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number - of seconds (e.g. ``120``) to cache the response. - cache_control: A ``cache-control`` header of type - :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. - cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization - of the cache key if caching is configured on the application level. - dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. - dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and - validation of request data. - etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. - exception_handlers: A mapping of status codes and/or exception types to handler functions. - guards: A sequence of :class:`Guard <.types.Guard>` callables. - http_method: An :class:`http method string <.types.Method>`, a member of the enum - :class:`HttpMethod ` or a list of these that correlates to the methods the - route handler function should handle. - media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a - valid IANA Media-Type. - middleware: A sequence of :class:`Middleware <.types.Middleware>`. - 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>`. - request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's - default request. - response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's - default response. - response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. - response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` - instances. - responses: A mapping of additional status codes and a description of their expected content. - This information will be included in the OpenAPI schema - return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing - outbound response data. - signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. - sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the - main event loop. This has an effect only for sync handler functions. See using sync handler functions. - content_encoding: A string describing the encoding of the content, e.g. ``base64``. - content_media_type: A string designating the media-type of the content, e.g. ``image/png``. - deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. - description: Text used for the route's schema description section. - include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. - operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. - operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. - raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. - This list should describe all exceptions raised within the route handler's function/method. The Litestar - ValidationException will be added automatically for the schema if any validation is involved. - response_description: Text used for the route's response schema description section. - security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. - summary: Text used for the route's schema summary section. - tags: A sequence of string tags that will be appended to the OpenAPI schema. - type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec - hook for deserialization. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. - """ - if "http_method" in kwargs: - raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) - super().__init__( + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, after_request=after_request, after_response=after_response, background=background, @@ -1094,3 +1068,172 @@ def __init__( type_encoders=type_encoders, **kwargs, ) + + return decorator + + +def delete( + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + handler_class: type[HTTPRouteHandler] = HTTPRouteHandler, + **kwargs: Any, +) -> Callable[[AnyCallable], HTTPRouteHandler]: + """Create an :class:`HTTPRouteHandler` with a ``DELETE`` method. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` + and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + handler_class: Route handler class instantiated by the decorator + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + + def decorator(fn: AnyCallable) -> HTTPRouteHandler: + return handler_class( + fn=fn, + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + etag=etag, + exception_handlers=exception_handlers, + guards=guards, + http_method=HttpMethod.DELETE, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + return decorator diff --git a/litestar/handlers/websocket_handlers/listener.py b/litestar/handlers/websocket_handlers/listener.py index 8e702ea1aa..aba965fc8d 100644 --- a/litestar/handlers/websocket_handlers/listener.py +++ b/litestar/handlers/websocket_handlers/listener.py @@ -42,8 +42,6 @@ if TYPE_CHECKING: from typing import Coroutine - from typing_extensions import Self - from litestar import Router from litestar.dto import AbstractDTO from litestar.types.asgi_types import WebSocketMode @@ -74,6 +72,7 @@ def __init__( self, path: str | list[str] | None = None, *, + fn: AnyCallable, connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -97,6 +96,7 @@ def __init__( self, path: str | list[str] | None = None, *, + fn: AnyCallable, connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -121,6 +121,7 @@ def __init__( self, path: str | list[str] | None = None, *, + fn: AnyCallable, connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, dependencies: Dependencies | None = None, @@ -146,6 +147,7 @@ def __init__( Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` + fn: The handler function connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``. connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default, @@ -210,6 +212,7 @@ def __init__( listener_dependencies["on_disconnect_dependencies"] = create_stub_dependency(self.on_disconnect) super().__init__( + fn=fn, path=path, dependencies=listener_dependencies, exception_handlers=exception_handlers, @@ -226,7 +229,7 @@ def __init__( **kwargs, ) - def __call__(self, fn: AnyCallable) -> Self: + def _prepare_fn(self, fn: AnyCallable) -> ListenerHandler: parsed_signature = ParsedSignature.from_fn(fn, self.resolve_signature_namespace()) if "data" not in parsed_signature.parameters: @@ -248,10 +251,8 @@ def __call__(self, fn: AnyCallable) -> Self: }, ) - return super().__call__( - ListenerHandler( - listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace() - ) + return ListenerHandler( + listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace() ) def _validate_handler_function(self) -> None: @@ -319,9 +320,6 @@ def resolve_send_handler(self) -> Callable[[WebSocket, Any], Coroutine[None, Non return self._send_handler -websocket_listener = WebsocketListenerRouteHandler - - class WebsocketListener(ABC): path: str | list[str] | None = None """A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/``""" @@ -398,7 +396,8 @@ def to_handler(self) -> WebsocketListenerRouteHandler: type_decoders=self.type_decoders, type_encoders=self.type_encoders, websocket_class=self.websocket_class, - )(self.on_receive) + fn=self.on_receive, + ) handler.owner = self._owner return handler @@ -414,3 +413,140 @@ def on_receive(self, *args: Any, **kwargs: Any) -> Any: according to handler configuration. """ raise NotImplementedError + + +@overload +def websocket_listener( + path: str | list[str] | None = None, + *, + connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, +) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]: ... + + +@overload +def websocket_listener( + path: str | list[str] | None = None, + *, + connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + on_accept: AnyCallable | None = None, + on_disconnect: AnyCallable | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, +) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]: ... + + +def websocket_listener( + path: str | list[str] | None = None, + *, + connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, + connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + on_accept: AnyCallable | None = None, + on_disconnect: AnyCallable | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, +) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]: + """Create a :class:`WebsocketListenerRouteHandler`. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance + and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``. + connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default, + it calls the ``connection_accept_handler``, ``on_connect`` and ``on_disconnect``. Can request any + dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + receive_mode: Websocket mode to receive data in, either `text` or `binary`. + send_mode: Websocket mode to receive data in, either `text` or `binary`. + name: A string identifying the route handler. + on_accept: Callback invoked after a connection has been accepted. Can request any dependencies, for example + the :class:`WebSocket <.connection.WebSocket>` connection + on_disconnect: Callback invoked after a connection has been closed. Can request any dependencies, for + example the :class:`WebSocket <.connection.WebSocket>` connection + 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>`. + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature + modelling. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + """ + + def decorator(fn: AnyCallable) -> WebsocketListenerRouteHandler: + return WebsocketListenerRouteHandler( + fn=fn, + path=path, + connection_accept_handler=connection_accept_handler, + connection_lifespan=connection_lifespan, + dependencies=dependencies, + dto=dto, + exception_handlers=exception_handlers, + guard=guards, + middleware=middleware, + receive_mode=receive_mode, + send_mode=send_mode, + name=name, + on_accept=on_accept, + on_disconnect=on_disconnect, + opt=opt, + return_dto=return_dto, + signature_namespace=signature_namespace, + type_decoders=type_decoders, + type_encoders=type_encoders, + websocket_class=websocket_class, + **kwargs, + ) + + return decorator diff --git a/litestar/handlers/websocket_handlers/route_handler.py b/litestar/handlers/websocket_handlers/route_handler.py index 2dd3a726ac..8ab76550f6 100644 --- a/litestar/handlers/websocket_handlers/route_handler.py +++ b/litestar/handlers/websocket_handlers/route_handler.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Mapping +from typing import TYPE_CHECKING, Any, Callable, Mapping from litestar.connection import WebSocket from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import BaseRouteHandler -from litestar.types import Empty +from litestar.types import AsyncAnyCallable, Empty from litestar.types.builtin_types import NoneType from litestar.utils.predicates import is_async_callable @@ -18,17 +18,13 @@ class WebsocketRouteHandler(BaseRouteHandler): - """Websocket route handler decorator. - - Use this decorator to decorate websocket handler functions. - """ - __slots__ = ("websocket_class", "_kwargs_model") def __init__( self, path: str | list[str] | None = None, *, + fn: AsyncAnyCallable, dependencies: Dependencies | None = None, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, @@ -39,11 +35,14 @@ def __init__( websocket_class: type[WebSocket] | None = None, **kwargs: Any, ) -> None: - """Initialize ``WebsocketRouteHandler`` + """Route handler for WebSocket routes. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` + fn: The handler function + + .. versionadded:: 3.0 dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. @@ -53,7 +52,6 @@ def __init__( 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. - type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. @@ -62,6 +60,7 @@ def __init__( self._kwargs_model: KwargsModel | EmptyType = Empty super().__init__( + fn=fn, path=path, dependencies=dependencies, exception_handlers=exception_handlers, @@ -145,4 +144,53 @@ async def handle(self, connection: WebSocket[Any, Any, Any]) -> None: await self.fn(**parsed_kwargs) -websocket = WebsocketRouteHandler +def websocket( + path: str | list[str] | None = None, + *, + dependencies: Dependencies | None = None, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + name: str | None = None, + opt: dict[str, Any] | None = None, + signature_namespace: Mapping[str, Any] | None = None, + websocket_class: type[WebSocket] | None = None, + handler_class: type[WebsocketRouteHandler] = WebsocketRouteHandler, + **kwargs: Any, +) -> Callable[[AsyncAnyCallable], WebsocketRouteHandler]: + """Create a :class:`WebsocketRouteHandler`. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + 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. + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + handler_class: Route handler class instantiated by the decorator + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + + def decorator(fn: AsyncAnyCallable) -> WebsocketRouteHandler: + return handler_class( + path=path, + fn=fn, + dependencies=dependencies, + exception_handlers=exception_handlers, + guards=guards, + middleware=middleware, + name=name, + opt=opt, + signature_namespace=signature_namespace, + websocket_class=websocket_class, + **kwargs, + ) + + return decorator diff --git a/litestar/testing/request_factory.py b/litestar/testing/request_factory.py index 49f2420b23..8899a94ad9 100644 --- a/litestar/testing/request_factory.py +++ b/litestar/testing/request_factory.py @@ -14,7 +14,7 @@ from litestar.enums import HttpMethod, ParamType, RequestEncodingType, ScopeType from litestar.handlers.http_handlers import get from litestar.serialization import decode_json, default_serializer, encode_json -from litestar.types import DataContainerType, HTTPScope, RouteHandlerType +from litestar.types import DataContainerType, HTTPHandlerDecorator, HTTPScope, RouteHandlerType from litestar.types.asgi_types import ASGIVersion from litestar.utils import get_serializer_from_scope from litestar.utils.scope.state import ScopeState @@ -25,7 +25,7 @@ from litestar.datastructures.cookie import Cookie from litestar.handlers.http_handlers import HTTPRouteHandler -_decorator_http_method_map: dict[HttpMethod, type[HTTPRouteHandler]] = { +_decorator_http_method_map: dict[HttpMethod, HTTPHandlerDecorator] = { HttpMethod.GET: get, HttpMethod.POST: post, HttpMethod.DELETE: delete, diff --git a/litestar/types/__init__.py b/litestar/types/__init__.py index 90e319277c..1e5197f605 100644 --- a/litestar/types/__init__.py +++ b/litestar/types/__init__.py @@ -54,6 +54,7 @@ ExceptionHandler, GetLogger, Guard, + HTTPHandlerDecorator, LifespanHook, OnAppInitHandler, OperationIDCreator, @@ -96,6 +97,7 @@ "DataContainerType", "DataclassProtocol", "Dependencies", + "HTTPHandlerDecorator", "Empty", "EmptyType", "ExceptionHandler", diff --git a/litestar/types/callable_types.py b/litestar/types/callable_types.py index 36055d7199..0f07295cc4 100644 --- a/litestar/types/callable_types.py +++ b/litestar/types/callable_types.py @@ -38,3 +38,4 @@ OnAppInitHandler: TypeAlias = "Callable[[AppConfig], AppConfig]" OperationIDCreator: TypeAlias = "Callable[[HTTPRouteHandler, Method, list[str | PathParameterDefinition]], str]" Serializer: TypeAlias = Callable[[Any], Any] +HTTPHandlerDecorator: TypeAlias = "Callable[..., Callable[[AnyCallable], HTTPRouteHandler]]" diff --git a/tests/e2e/test_router_registration.py b/tests/e2e/test_router_registration.py index be4858f66a..357411108f 100644 --- a/tests/e2e/test_router_registration.py +++ b/tests/e2e/test_router_registration.py @@ -14,7 +14,9 @@ put, websocket, ) -from litestar import route as route_decorator +from litestar import ( + route as route_decorator, +) from litestar.exceptions import ImproperlyConfiguredException from litestar.routes import HTTPRoute diff --git a/tests/e2e/test_routing/test_path_resolution.py b/tests/e2e/test_routing/test_path_resolution.py index 8b25694956..4ffdc004a8 100644 --- a/tests/e2e/test_routing/test_path_resolution.py +++ b/tests/e2e/test_routing/test_path_resolution.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Callable, List, Optional, Type +from typing import Any, Callable, List, Optional import httpx import pytest @@ -75,29 +75,27 @@ def mixed_params(path_param: int, value: int) -> str: @pytest.mark.parametrize( - "decorator, test_path, decorator_path, delete_handler", + "test_path, decorator_path, delete_handler", [ - (get, "", "/something", None), - (get, "/", "/something", None), - (get, "", "/", None), - (get, "/", "/", None), - (get, "", "", None), - (get, "/", "", None), - (get, "", "/something", root_delete_handler), - (get, "/", "/something", root_delete_handler), - (get, "", "/", root_delete_handler), - (get, "/", "/", root_delete_handler), - (get, "", "", root_delete_handler), - (get, "/", "", root_delete_handler), + ("", "/something", None), + ("/", "/something", None), + ("", "/", None), + ("/", "/", None), + ("", "", None), + ("/", "", None), + ("", "/something", root_delete_handler), + ("/", "/something", root_delete_handler), + ("", "/", root_delete_handler), + ("/", "/", root_delete_handler), + ("", "", root_delete_handler), + ("/", "", root_delete_handler), ], ) -def test_root_route_handler( - decorator: Type[get], test_path: str, decorator_path: str, delete_handler: Optional[Callable] -) -> None: +def test_root_route_handler(test_path: str, decorator_path: str, delete_handler: Optional[Callable]) -> None: class MyController(Controller): path = test_path - @decorator(path=decorator_path) + @get(path=decorator_path) def test_method(self) -> str: return "hello" diff --git a/tests/e2e/test_routing/test_route_indexing.py b/tests/e2e/test_routing/test_route_indexing.py index 952579c3a7..f56c4ecc89 100644 --- a/tests/e2e/test_routing/test_route_indexing.py +++ b/tests/e2e/test_routing/test_route_indexing.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Type +from typing import TYPE_CHECKING, Any import pytest @@ -15,15 +15,15 @@ websocket, ) from litestar.exceptions import ImproperlyConfiguredException -from litestar.handlers.http_handlers import HTTPRouteHandler +from litestar.types import HTTPHandlerDecorator if TYPE_CHECKING: from pathlib import Path @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) -def test_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None: - @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg] +def test_indexes_handlers(decorator: HTTPHandlerDecorator) -> None: + @decorator("/path-one/{param:str}", name="handler-name") def handler() -> None: return None @@ -57,19 +57,19 @@ async def websocket_handler(socket: Any) -> None: @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) -def test_default_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None: - @decorator("/handler") # type: ignore[call-arg] +def test_default_indexes_handlers(decorator: HTTPHandlerDecorator) -> None: + @decorator("/handler") def handler() -> None: pass - @decorator("/named_handler", name="named_handler") # type: ignore[call-arg] + @decorator("/named_handler", name="named_handler") def named_handler() -> None: pass class MyController(Controller): path = "/test" - @decorator() # type: ignore[call-arg] + @decorator() def handler(self) -> None: pass @@ -93,12 +93,12 @@ def handler(self) -> None: @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) -def test_indexes_handlers_with_multiple_paths(decorator: Type[HTTPRouteHandler]) -> None: - @decorator(["/path-one", "/path-one/{param:str}"], name="handler") # type: ignore[call-arg] +def test_indexes_handlers_with_multiple_paths(decorator: HTTPHandlerDecorator) -> None: + @decorator(["/path-one", "/path-one/{param:str}"], name="handler") def handler() -> None: return None - @decorator(["/path-two"], name="handler-two") # type: ignore[call-arg] + @decorator(["/path-two"], name="handler-two") def handler_two() -> None: return None diff --git a/tests/e2e/test_routing/test_route_reverse.py b/tests/e2e/test_routing/test_route_reverse.py index 0a8d914883..32b948cdc2 100644 --- a/tests/e2e/test_routing/test_route_reverse.py +++ b/tests/e2e/test_routing/test_route_reverse.py @@ -1,35 +1,34 @@ from datetime import time -from typing import Type import pytest from litestar import Litestar, Router, delete, get, patch, post, put from litestar.exceptions import NoRouteMatchFoundException -from litestar.handlers.http_handlers import HTTPRouteHandler +from litestar.types import HTTPHandlerDecorator @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) -def test_route_reverse(decorator: Type[HTTPRouteHandler]) -> None: - @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg] +def test_route_reverse(decorator: HTTPHandlerDecorator) -> None: + @decorator("/path-one/{param:str}", name="handler-name") def handler() -> None: return None - @decorator("/path-two", name="handler-no-params") # type: ignore[call-arg] + @decorator("/path-two", name="handler-no-params") def handler_no_params() -> None: return None - @decorator("/multiple/{str_param:str}/params/{int_param:int}/", name="multiple-params-handler-name") # type: ignore[call-arg] + @decorator("/multiple/{str_param:str}/params/{int_param:int}/", name="multiple-params-handler-name") def handler2() -> None: return None @decorator( ["/handler3", "/handler3/{str_param:str}/", "/handler3/{str_param:str}/{int_param:int}/"], name="multiple-default-params", - ) # type: ignore[call-arg] + ) def handler3(str_param: str = "default", int_param: int = 0) -> None: return None - @decorator(["/handler4/int/{int_param:int}", "/handler4/str/{str_param:str}"], name="handler4") # type: ignore[call-arg] + @decorator(["/handler4/int/{int_param:int}", "/handler4/str/{str_param:str}"], name="handler4") def handler4(int_param: int = 1, str_param: str = "str") -> None: return None diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index e049608f24..cc9b15d2a9 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -1,4 +1,4 @@ -from typing import Any, Type, Union +from typing import Any import msgspec import pytest @@ -19,6 +19,7 @@ from litestar.exceptions import ImproperlyConfiguredException from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.testing import create_test_client +from litestar.types import HTTPHandlerDecorator from tests.models import DataclassPerson, DataclassPersonFactory @@ -40,7 +41,7 @@ ], ) async def test_controller_http_method( - decorator: Union[Type[get], Type[post], Type[put], Type[patch], Type[delete]], + decorator: HTTPHandlerDecorator, http_method: HttpMethod, expected_status_code: int, return_value: Any, @@ -51,7 +52,7 @@ async def test_controller_http_method( class MyController(Controller): path = test_path - @decorator() # type: ignore[misc] + @decorator() def test_method(self) -> return_annotation: return return_value diff --git a/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py b/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py index 84d9320c98..1230399cef 100644 --- a/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py +++ b/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py @@ -1,5 +1,6 @@ from litestar import Controller, MediaType, asgi from litestar.enums import ScopeType +from litestar.handlers import ASGIRouteHandler from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client @@ -51,3 +52,14 @@ async def root_asgi_handler( response = client.get("/asgi") assert response.status_code == HTTP_200_OK assert response.text == "/asgi" + + +def test_custom_handler_class() -> None: + class MyHandlerClass(ASGIRouteHandler): + pass + + @asgi("/", handler_class=MyHandlerClass) + async def handler() -> None: + pass + + assert isinstance(handler, MyHandlerClass) diff --git a/tests/unit/test_handlers/test_base_handlers/test_opt.py b/tests/unit/test_handlers/test_base_handlers/test_opt.py index 453eb00889..60558ff2f6 100644 --- a/tests/unit/test_handlers/test_base_handlers/test_opt.py +++ b/tests/unit/test_handlers/test_base_handlers/test_opt.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from litestar import WebSocket - from litestar.types import Receive, RouteHandlerType, Scope, Send + from litestar.types import AnyCallable, Receive, RouteHandlerType, Scope, Send def regular_handler() -> None: ... @@ -41,9 +41,11 @@ async def socket_handler(socket: "WebSocket") -> None: ... (websocket, socket_handler), ], ) -def test_opt_settings(decorator: "RouteHandlerType", handler: Callable) -> None: +def test_opt_settings( + decorator: Callable[..., Callable[["AnyCallable"], "RouteHandlerType"]], handler: "Callable" +) -> None: base_opt = {"base": 1, "kwarg_value": 0} - result = decorator("/", opt=base_opt, kwarg_value=2)(handler) # type: ignore[arg-type, call-arg] + result = decorator("/", opt=base_opt, kwarg_value=2)(handler) assert result.opt == {"base": 1, "kwarg_value": 2} diff --git a/tests/unit/test_handlers/test_base_handlers/test_validations.py b/tests/unit/test_handlers/test_base_handlers/test_validations.py index a0b168a230..e4f9cc5e06 100644 --- a/tests/unit/test_handlers/test_base_handlers/test_validations.py +++ b/tests/unit/test_handlers/test_base_handlers/test_validations.py @@ -5,14 +5,6 @@ from litestar import Litestar, post from litestar.dto import DTOData from litestar.exceptions import ImproperlyConfiguredException -from litestar.handlers.base import BaseRouteHandler - - -def test_raise_no_fn_validation() -> None: - handler = BaseRouteHandler(path="/") - - with pytest.raises(ImproperlyConfiguredException): - handler.fn def test_dto_data_annotation_with_no_resolved_dto() -> None: diff --git a/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py b/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py new file mode 100644 index 0000000000..3df1d6954f --- /dev/null +++ b/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py @@ -0,0 +1,30 @@ +from typing import Callable + +import pytest + +from litestar.handlers import HTTPRouteHandler +from litestar.handlers.http_handlers import delete, get, patch, post, put, route +from litestar.types import AnyCallable + + +@pytest.mark.parametrize("handler_decorator", [get, put, delete, post, patch]) +def test_custom_handler_class(handler_decorator: Callable[..., Callable[[AnyCallable], HTTPRouteHandler]]) -> None: + class MyHandlerClass(HTTPRouteHandler): + pass + + @handler_decorator("/", handler_class=MyHandlerClass) + async def handler() -> None: + pass + + assert isinstance(handler, MyHandlerClass) + + +def test_custom_handler_class_route() -> None: + class MyHandlerClass(HTTPRouteHandler): + pass + + @route("/", handler_class=MyHandlerClass, http_method="GET") + async def handler() -> None: + pass + + assert isinstance(handler, MyHandlerClass) diff --git a/tests/unit/test_handlers/test_http_handlers/test_defaults.py b/tests/unit/test_handlers/test_http_handlers/test_defaults.py index 06c0387f98..314e131ccc 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_defaults.py +++ b/tests/unit/test_handlers/test_http_handlers/test_defaults.py @@ -37,5 +37,5 @@ ], ) def test_route_handler_default_status_code(http_method: Any, expected_status_code: int) -> None: - route_handler = HTTPRouteHandler(http_method=http_method) + route_handler = HTTPRouteHandler(http_method=http_method, fn=lambda: None) assert route_handler.status_code == expected_status_code diff --git a/tests/unit/test_handlers/test_http_handlers/test_head.py b/tests/unit/test_handlers/test_http_handlers/test_head.py index 8cc2e7b2a5..76cadaacf5 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_head.py +++ b/tests/unit/test_handlers/test_http_handlers/test_head.py @@ -2,7 +2,7 @@ import pytest -from litestar import HttpMethod, Litestar, head +from litestar import Litestar, head from litestar.exceptions import ImproperlyConfiguredException from litestar.response.file import ASGIFileResponse, File from litestar.routes import HTTPRoute @@ -30,16 +30,6 @@ def handler() -> dict: handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler])) -def test_head_decorator_raises_validation_error_if_method_is_passed() -> None: - with pytest.raises(ImproperlyConfiguredException): - - @head("/", http_method=HttpMethod.HEAD) - def handler() -> None: - return - - handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler])) - - def test_head_decorator_does_not_raise_for_file_response() -> None: @head("/") def handler() -> "File": diff --git a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py index 73ed586b50..326972ecac 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py +++ b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py @@ -4,11 +4,10 @@ from hypothesis import given from hypothesis import strategies as st -from litestar import HttpMethod, MediaType, Response, delete, get, patch, post, put +from litestar import HttpMethod, MediaType, Response from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.http_handlers._utils import get_default_status_code -from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.utils import normalize_path @@ -36,9 +35,9 @@ def test_route_handler_kwarg_handling( ) -> None: if not http_method: with pytest.raises(ImproperlyConfiguredException): - HTTPRouteHandler(http_method=http_method) + HTTPRouteHandler(http_method=http_method, fn=dummy_method) else: - decorator = HTTPRouteHandler( + result = HTTPRouteHandler( http_method=http_method, media_type=media_type, include_in_schema=include_in_schema, @@ -46,8 +45,8 @@ def test_route_handler_kwarg_handling( response_headers=response_headers, status_code=status_code, path=path, + fn=dummy_method, ) - result = decorator(dummy_method) if isinstance(http_method, list): assert all(method in result.http_methods for method in http_method) else: @@ -61,24 +60,3 @@ def test_route_handler_kwarg_handling( else: assert next(iter(result.paths)) == normalize_path(path) assert result.status_code == status_code or get_default_status_code(http_methods=result.http_methods) - - -@pytest.mark.parametrize( - "sub, http_method, expected_status_code", - [ - (post, HttpMethod.POST, HTTP_201_CREATED), - (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), - (get, HttpMethod.GET, HTTP_200_OK), - (put, HttpMethod.PUT, HTTP_200_OK), - (patch, HttpMethod.PATCH, HTTP_200_OK), - ], -) -def test_semantic_route_handlers_disallow_http_method_assignment( - sub: Any, http_method: Any, expected_status_code: int -) -> None: - result = sub()(dummy_method) - assert http_method in result.http_methods - assert result.status_code == expected_status_code - - with pytest.raises(ImproperlyConfiguredException): - sub(http_method=HttpMethod.GET if http_method != HttpMethod.GET else HttpMethod.POST) diff --git a/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py b/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py index eff63bf2b0..112af4ed24 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py +++ b/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py @@ -6,17 +6,25 @@ from litestar import Controller, Router, delete, get, patch, post, put from litestar.testing import create_test_client +from litestar.types import HTTPHandlerDecorator @pytest.mark.parametrize( - ("method", "decorator"), [("GET", get), ("PUT", put), ("POST", post), ("PATCH", patch), ("DELETE", delete)] + ("method", "decorator"), + [ + ("GET", get), + ("PUT", put), + ("POST", post), + ("PATCH", patch), + ("DELETE", delete), + ], ) -def test_websocket_signature_namespace(method: str, decorator: type[get | put | post | patch | delete]) -> None: +def test_websocket_signature_namespace(method: str, decorator: HTTPHandlerDecorator) -> None: class MyController(Controller): path = "/" signature_namespace = {"c": float} - @decorator(path="/", signature_namespace={"d": List[str], "dict": Dict}, status_code=200) # type:ignore[misc] + @decorator(path="/", signature_namespace={"d": List[str], "dict": Dict}, status_code=200) async def simple_handler( self, a: a, # type:ignore[name-defined] # noqa: F821 diff --git a/tests/unit/test_handlers/test_http_handlers/test_validations.py b/tests/unit/test_handlers/test_http_handlers/test_validations.py index f0f492ee72..0ae5c233ed 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_validations.py +++ b/tests/unit/test_handlers/test_http_handlers/test_validations.py @@ -21,19 +21,19 @@ def test_route_handler_validation_http_method() -> None: # doesn't raise for http methods for value in (*list(HttpMethod), *[x.upper() for x in list(HttpMethod)]): - assert route(http_method=value) # type: ignore[arg-type, truthy-bool] + assert route(http_method=value) # type: ignore[arg-type, truthy-function] # raises for invalid values with pytest.raises(ValidationException): - HTTPRouteHandler(http_method="deleze") # type: ignore[arg-type] + HTTPRouteHandler(http_method="deleze", fn=lambda: None) # type: ignore[arg-type] # also when passing an empty list with pytest.raises(ImproperlyConfiguredException): - route(http_method=[], status_code=HTTP_200_OK) + HTTPRouteHandler(http_method=[], status_code=HTTP_200_OK, fn=lambda: None) # also when passing malformed tokens with pytest.raises(ValidationException): - route(http_method=[HttpMethod.GET, "poft"], status_code=HTTP_200_OK) # type: ignore[list-item] + HTTPRouteHandler(http_method=[HttpMethod.GET, "poft"], status_code=HTTP_200_OK, fn=lambda: None) # type: ignore[list-item] async def test_function_validation() -> None: diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py b/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py new file mode 100644 index 0000000000..e4b3afcb89 --- /dev/null +++ b/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py @@ -0,0 +1,12 @@ +from litestar.handlers import WebsocketRouteHandler, websocket + + +def test_custom_handler_class() -> None: + class MyHandlerClass(WebsocketRouteHandler): + pass + + @websocket("/", handler_class=MyHandlerClass) + async def handler() -> None: + pass + + assert isinstance(handler, MyHandlerClass) diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py index 46d27c1847..52b8e1dce0 100644 --- a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py +++ b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py @@ -11,6 +11,7 @@ from litestar.di import Provide from litestar.dto import DataclassDTO, dto_field from litestar.exceptions import ImproperlyConfiguredException +from litestar.handlers import WebsocketListenerRouteHandler from litestar.handlers.websocket_handlers import WebsocketListener, websocket_listener from litestar.routes import WebSocketRoute from litestar.testing import create_test_client @@ -28,21 +29,21 @@ def on_receive(self, data: str) -> str: # pyright: ignore @pytest.fixture -def sync_listener_callable(mock: MagicMock) -> websocket_listener: +def sync_listener_callable(mock: MagicMock) -> WebsocketListenerRouteHandler: def listener(data: str) -> str: mock(data) return data - return websocket_listener("/")(listener) + return WebsocketListenerRouteHandler("/", fn=listener) @pytest.fixture -def async_listener_callable(mock: MagicMock) -> websocket_listener: +def async_listener_callable(mock: MagicMock) -> WebsocketListenerRouteHandler: async def listener(data: str) -> str: mock(data) return data - return websocket_listener("/")(listener) + return WebsocketListenerRouteHandler("/", fn=listener) @pytest.mark.parametrize( @@ -53,7 +54,9 @@ async def listener(data: str) -> str: lf("listener_class"), ], ) -def test_basic_listener(mock: MagicMock, listener: Union[websocket_listener, Type[WebsocketListener]]) -> None: +def test_basic_listener( + mock: MagicMock, listener: Union[WebsocketListenerRouteHandler, Type[WebsocketListener]] +) -> None: client = create_test_client([listener]) with client.websocket_connect("/") as ws: ws.send_text("foo") diff --git a/tests/unit/test_kwargs/test_validations.py b/tests/unit/test_kwargs/test_validations.py index f0d524b0ec..0853be05c6 100644 --- a/tests/unit/test_kwargs/test_validations.py +++ b/tests/unit/test_kwargs/test_validations.py @@ -48,17 +48,17 @@ def test_raises_when_reserved_kwargs_are_misused(reserved_kwarg: str) -> None: decorator = post if reserved_kwarg != "socket" else websocket exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass") - handler_with_path_param = decorator("/{" + reserved_kwarg + ":int}")(locals()["test_fn"]) + handler_with_path_param = decorator("/{" + reserved_kwarg + ":int}")(locals()["test_fn"]) # type: ignore[operator] with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_path_param]) exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass") - handler_with_dependency = decorator("/", dependencies={reserved_kwarg: Provide(my_dependency)})(locals()["test_fn"]) + handler_with_dependency = decorator("/", dependencies={reserved_kwarg: Provide(my_dependency)})(locals()["test_fn"]) # type: ignore[operator] with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_dependency]) exec(f"async def test_fn({reserved_kwarg}: int = Parameter(query='my_param')) -> None: pass") - handler_with_aliased_param = decorator("/")(locals()["test_fn"]) + handler_with_aliased_param = decorator("/")(locals()["test_fn"]) # type: ignore[operator] with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_aliased_param]) diff --git a/tests/unit/test_middleware/test_rate_limit_middleware.py b/tests/unit/test_middleware/test_rate_limit_middleware.py index 0c7a8a2ddb..c16577b2da 100644 --- a/tests/unit/test_middleware/test_rate_limit_middleware.py +++ b/tests/unit/test_middleware/test_rate_limit_middleware.py @@ -212,7 +212,7 @@ def handler() -> None: path1 = tmpdir / "test.css" path1.write_text("styles content", "utf-8") - asgi_handler = ASGIRouteHandler("/asgi", is_mount=True)(ASGIResponse(body="something")) + asgi_handler = ASGIRouteHandler("/asgi", is_mount=True, fn=ASGIResponse(body="something")) rate_limit_config = RateLimitConfig(rate_limit=("minute", 1), exclude=[r"^/src.*$"]) with create_test_client([handler, asgi_handler], middleware=[rate_limit_config.middleware]) as client: diff --git a/tests/unit/test_openapi/test_path_item.py b/tests/unit/test_openapi/test_path_item.py index 1f0dadc5b3..a2401b1c86 100644 --- a/tests/unit/test_openapi/test_path_item.py +++ b/tests/unit/test_openapi/test_path_item.py @@ -8,12 +8,11 @@ import pytest from typing_extensions import TypeAlias -from litestar import Controller, HttpMethod, Litestar, Request, Router, delete, get +from litestar import Controller, HttpMethod, Litestar, Request, Router, delete, get, route from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.path_item import PathItemFactory, merge_path_item_operations from litestar._openapi.utils import default_operation_id_creator from litestar.exceptions import ImproperlyConfiguredException -from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import Operation, PathItem from litestar.utils import find_index @@ -23,7 +22,7 @@ @pytest.fixture() -def route(person_controller: type[Controller]) -> HTTPRoute: +def http_route(person_controller: type[Controller]) -> HTTPRoute: app = Litestar(route_handlers=[person_controller], openapi_config=None) index = find_index(app.routes, lambda x: x.path_format == "/{service_id}/person/{person_id}") return cast("HTTPRoute", app.routes[index]) @@ -59,8 +58,8 @@ def factory(route: HTTPRoute) -> PathItemFactory: return factory -def test_create_path_item(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: - schema = create_factory(route).create_path_item() +def test_create_path_item(http_route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: + schema = create_factory(http_route).create_path_item() assert schema.delete assert schema.delete.operation_id == "ServiceIdPersonPersonIdDeletePerson" assert schema.delete.summary == "DeletePerson" @@ -79,7 +78,7 @@ def test_unique_operation_ids_for_multiple_http_methods(create_factory: CreateFa class MultipleMethodsRouteController(Controller): path = "/" - @HTTPRouteHandler("/", http_method=["GET", "HEAD"]) + @route("/", http_method=["GET", "HEAD"]) async def root(self, *, request: Request[str, str, Any]) -> None: pass @@ -100,7 +99,7 @@ def test_unique_operation_ids_for_multiple_http_methods_with_handler_level_opera class MultipleMethodsRouteController(Controller): path = "/" - @HTTPRouteHandler("/", http_method=["GET", "HEAD"], operation_id=default_operation_id_creator) + @route("/", http_method=["GET", "HEAD"], operation_id=default_operation_id_creator) async def root(self, *, request: Request[str, str, Any]) -> None: pass @@ -128,8 +127,10 @@ def test_routes_with_different_paths_should_generate_unique_operation_ids( assert schema_v1.get.operation_id != schema_v2.get.operation_id -def test_create_path_item_use_handler_docstring_false(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: - factory = create_factory(route) +def test_create_path_item_use_handler_docstring_false( + http_route: HTTPRoute, create_factory: CreateFactoryFixture +) -> None: + factory = create_factory(http_route) assert not factory.context.openapi_config.use_handler_docstrings schema = factory.create_path_item() assert schema.get @@ -138,8 +139,10 @@ def test_create_path_item_use_handler_docstring_false(route: HTTPRoute, create_f assert schema.patch.description == "Description in decorator" -def test_create_path_item_use_handler_docstring_true(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: - factory = create_factory(route) +def test_create_path_item_use_handler_docstring_true( + http_route: HTTPRoute, create_factory: CreateFactoryFixture +) -> None: + factory = create_factory(http_route) factory.context.openapi_config.use_handler_docstrings = True schema = factory.create_path_item() assert schema.get diff --git a/tests/unit/test_signature/test_validation.py b/tests/unit/test_signature/test_validation.py index cd30f89df8..ef618dd10b 100644 --- a/tests/unit/test_signature/test_validation.py +++ b/tests/unit/test_signature/test_validation.py @@ -120,7 +120,7 @@ def handler(data: Parent) -> None: model = SignatureModel.create( dependency_name_set=set(), - fn=handler, + fn=handler, # type: ignore[arg-type] data_dto=None, parsed_signature=ParsedSignature.from_fn(handler.fn, {}), type_decoders=[],