Skip to content

Commit

Permalink
refactor(routing)!: Refactor routes and route handlers (#3386)
Browse files Browse the repository at this point in the history
* remove handler names
* Remove option handler creation from HTTPRoute
* Remove methods attribute from BaseRoute
* Move kwargs model to handlers and creation to on_registration
* Store kwargs model on handlers instead of routes
* Simplify HTTPRoute route_handler_map creation
* Simplify Router.route_handler_method_map
* Relax typing of HTTPRoute
* Move handling logic to route handlers
* Remove scope_type
* Don't pass route to HTTPRouteHandler during handling
* Don't pass scope to handle methods
* Resolve and establish connections in routes; Only pass connections to handlers

---------

Co-authored-by: Jacob Coffee <[email protected]>
Co-authored-by: Peter Schutt <[email protected]>
  • Loading branch information
3 people committed Sep 14, 2024
1 parent cfefa25 commit 59a00da
Show file tree
Hide file tree
Showing 30 changed files with 430 additions and 391 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
(PY_RE, r"advanced_alchemy\.config.common\.EngineT"),
(PY_RE, r"advanced_alchemy\.config.common\.SessionT"),
(PY_RE, r".*R"),
(PY_RE, r".*ScopeT"),
(PY_OBJ, r"litestar.security.jwt.auth.TokenT"),
(PY_CLASS, "ExceptionToProblemDetailMapType"),
(PY_CLASS, "litestar.security.jwt.token.JWTDecodeOptions"),
Expand Down
3 changes: 1 addition & 2 deletions litestar/_asgi/routing_trie/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ def configure_node(
node.path_parameters = {}

if isinstance(route, HTTPRoute):
for method, handler_mapping in route.route_handler_map.items():
handler, _ = handler_mapping
for method, handler in route.route_handler_map.items():
node.asgi_handlers[method] = ASGIHandlerTuple(
asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=handler),
handler=handler,
Expand Down
4 changes: 1 addition & 3 deletions litestar/_openapi/path_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ def create_path_item(self) -> PathItem:
Returns:
A PathItem instance.
"""
for http_method, handler_tuple in self.route.route_handler_map.items():
route_handler, _ = handler_tuple

for http_method, route_handler in self.route.route_handler_map.items():
if not route_handler.resolve_include_in_schema():
continue

Expand Down
2 changes: 1 addition & 1 deletion litestar/_openapi/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def receive_route(self, route: BaseRoute) -> None:
if not isinstance(route, HTTPRoute):
return

if any(route_handler.resolve_include_in_schema() for route_handler, _ in route.route_handler_map.values()):
if any(route_handler.resolve_include_in_schema() for route_handler in route.route_handler_map.values()):
# Force recompute the schema if a new route is added
self._openapi = None
self.included_routes[route.path] = route
11 changes: 2 additions & 9 deletions litestar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
)
from litestar.plugins.base import CLIPlugin
from litestar.router import Router
from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
from litestar.stores.registry import StoreRegistry
from litestar.types import Empty, TypeDecodersSequence
from litestar.types.internal_types import PathParameterDefinition, TemplateConfigType
Expand All @@ -67,6 +66,7 @@
from litestar.openapi.spec import SecurityRequirement
from litestar.openapi.spec.open_api import OpenAPI
from litestar.response import Response
from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
from litestar.stores.base import Store
from litestar.types import (
AfterExceptionHookHandler,
Expand Down Expand Up @@ -668,14 +668,7 @@ def register(self, value: ControllerRouterHandler) -> None: # type: ignore[over
route_handlers = get_route_handlers(route)

for route_handler in route_handlers:
route_handler.on_registration(self)

if isinstance(route, HTTPRoute):
route.create_handler_map()

elif isinstance(route, WebSocketRoute):
handler = route.route_handler
route.handler_parameter_model = handler.create_kwargs_model(path_parameters=route.path_parameters)
route_handler.on_registration(self, route=route)

for plugin in self.plugins.receive_route:
plugin.receive_route(route)
Expand Down
18 changes: 18 additions & 0 deletions litestar/handlers/asgi_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@


if TYPE_CHECKING:
from litestar.connection import ASGIConnection
from litestar.types import (
ExceptionHandlersMap,
Guard,
Expand Down Expand Up @@ -82,5 +83,22 @@ def _validate_handler_function(self) -> None:
if not is_async_callable(self.fn):
raise ImproperlyConfiguredException("Functions decorated with 'asgi' must be async functions")

async def handle(self, connection: ASGIConnection[ASGIRouteHandler, Any, Any, Any]) -> None:
"""ASGI app that authorizes the connection and then awaits the handler function.
.. versionadded: 3.0
Args:
connection: The ASGI connection
Returns:
None
"""

if self.resolve_guards():
await self.authorize_connection(connection=connection)

await self.fn(scope=connection.scope, receive=connection.receive, send=connection.send)


asgi = ASGIRouteHandler
13 changes: 7 additions & 6 deletions litestar/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from copy import copy
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, cast
from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Sequence, cast

from litestar._signature import SignatureModel
from litestar.di import Provide
Expand Down Expand Up @@ -34,9 +34,9 @@
from litestar.dto import AbstractDTO
from litestar.params import ParameterKwarg
from litestar.router import Router
from litestar.routes import BaseRoute
from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler
from litestar.types.empty import EmptyType
from litestar.types.internal_types import PathParameterDefinition

__all__ = ("BaseRouteHandler",)

Expand Down Expand Up @@ -526,11 +526,12 @@ def _validate_dependency_is_unique(dependencies: dict[str, Provide], key: str, p
f"If you wish to override a provider, it must have the same key."
)

def on_registration(self, app: Litestar) -> None:
def on_registration(self, app: Litestar, route: BaseRoute) -> None:
"""Called once per handler when the app object is instantiated.
Args:
app: The :class:`Litestar<.app.Litestar>` app object.
route: The route this handler is being registered on
Returns:
None
Expand Down Expand Up @@ -567,9 +568,9 @@ def __str__(self) -> str:
target = type(target)
return f"{target.__module__}.{target.__qualname__}"

def create_kwargs_model(
def _create_kwargs_model(
self,
path_parameters: dict[str, PathParameterDefinition],
path_parameters: Iterable[str],
) -> KwargsModel:
"""Create a `KwargsModel` for a given route handler."""
from litestar._kwargs import KwargsModel
Expand All @@ -578,6 +579,6 @@ def create_kwargs_model(
signature_model=self.signature_model,
parsed_signature=self.parsed_fn_signature,
dependencies=self.resolve_dependencies(),
path_parameters=set(path_parameters.keys()),
path_parameters=set(path_parameters),
layered_parameters=self.resolve_layered_parameters(),
)
40 changes: 40 additions & 0 deletions litestar/handlers/http_handlers/_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Iterable

from litestar.enums import HttpMethod, MediaType
from litestar.handlers import HTTPRouteHandler
from litestar.response import Response
from litestar.status_codes import HTTP_204_NO_CONTENT

if TYPE_CHECKING:
from litestar.types import Method


def create_options_handler(path: str, allow_methods: Iterable[Method]) -> HTTPRouteHandler:
"""Args:
path: The route path
Returns:
An HTTP route handler for OPTIONS requests.
"""

def options_handler() -> Response:
"""Handler function for OPTIONS requests.
Returns:
Response
"""
return Response(
content=None,
status_code=HTTP_204_NO_CONTENT,
headers={"Allow": ", ".join(sorted(allow_methods))}, # pyright: ignore
media_type=MediaType.TEXT,
)

return HTTPRouteHandler(
path=path,
http_method=[HttpMethod.OPTIONS],
include_in_schema=False,
sync_to_thread=False,
)(options_handler)
7 changes: 7 additions & 0 deletions litestar/handlers/http_handlers/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Sequence, cast

from litestar.datastructures import UploadFile
from litestar.enums import HttpMethod
from litestar.exceptions import ValidationException
from litestar.response import Response
Expand Down Expand Up @@ -215,3 +216,9 @@ def is_empty_response_annotation(return_annotation: FieldDefinition) -> bool:


HTTP_METHOD_NAMES = {m.value for m in HttpMethod}


async def cleanup_temporary_files(form_data: dict[str, Any]) -> None:
for v in form_data.values():
if isinstance(v, UploadFile) and not v.file.closed:
await v.close()
Loading

0 comments on commit 59a00da

Please sign in to comment.