Skip to content

Commit

Permalink
refactor!: decouple Xpresso Router from Starlette Router (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangb authored Feb 3, 2022
1 parent 61bb006 commit a4c4dbe
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 51 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "xpresso"
version = "0.10.1"
version = "0.11.0"
description = "A developer centric, performant Python web framework"
authors = ["Adrian Garcia Badaracco <[email protected]>"]
readme = "README.md"
Expand Down
12 changes: 0 additions & 12 deletions xpresso/_utils/deprecation.py

This file was deleted.

13 changes: 8 additions & 5 deletions xpresso/_utils/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
from starlette.routing import Router as StarletteRouter

from xpresso.routing.pathitem import Path
from xpresso.routing.router import Router as XpressoRouter
from xpresso.routing.websockets import WebSocketRoute

Router = typing.Union[XpressoRouter, StarletteRouter]


class App(Protocol):
@property
def router(self) -> StarletteRouter:
def router(self) -> XpressoRouter:
...


Expand All @@ -26,21 +29,21 @@ def router(self) -> StarletteRouter:
@dataclass(frozen=True)
class VisitedRoute(typing.Generic[AppType]):
path: str
nodes: typing.List[typing.Union[StarletteRouter, AppType]]
nodes: typing.List[typing.Union[Router, AppType]]
route: BaseRoute


def visit_routes(
app_type: typing.Type[AppType],
router: StarletteRouter,
nodes: typing.List[typing.Union[StarletteRouter, AppType]],
router: Router,
nodes: typing.List[typing.Union[Router, AppType]],
path: str,
) -> typing.Generator[VisitedRoute[AppType], None, None]:
for route in typing.cast(typing.Iterable[BaseRoute], router.routes): # type: ignore # for Pylance
if isinstance(route, Mount):
app: typing.Any = route.app
mount_path: str = route.path # type: ignore # for Pylance
if isinstance(app, StarletteRouter):
if isinstance(app, (StarletteRouter, XpressoRouter)):
yield VisitedRoute(
path=path,
nodes=nodes + [app],
Expand Down
16 changes: 6 additions & 10 deletions xpresso/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,11 @@ def _setup(self) -> typing.List[typing.Callable[..., typing.AsyncIterator[None]]
for node in route.nodes:
if isinstance(node, Router):
dependencies.extend(node.dependencies)
if node is not self.router: # avoid circul lifespan calls
lifespan = typing.cast(
typing.Callable[..., typing.AsyncContextManager[None]],
node.lifespan_context, # type: ignore # for Pylance
# avoid circular lifespan calls
if node is not self.router and node.lifespan is not None:
lifespans.append(
_wrap_lifespan_as_async_generator(node.lifespan)
)
if lifespan is not None:
lifespans.append(
_wrap_lifespan_as_async_generator(lifespan)
)
if isinstance(route.route, Path):
for operation in route.route.operations.values():
operation.solve(
Expand All @@ -236,7 +232,7 @@ def _setup(self) -> typing.List[typing.Callable[..., typing.AsyncIterator[None]]
)
return lifespans

async def get_openapi(self) -> openapi_models.OpenAPI:
def get_openapi(self) -> openapi_models.OpenAPI:
return genrate_openapi(
visitor=visit_routes(
app_type=App, router=self.router, nodes=[self, self.router], path=""
Expand All @@ -259,7 +255,7 @@ def _get_doc_routes(

async def openapi(req: Request) -> JSONResponse:
if self.openapi is None:
self.openapi = await self.get_openapi()
self.openapi = self.get_openapi()
res = JSONResponse(self.openapi.dict(exclude_none=True, by_alias=True))
return res

Expand Down
66 changes: 43 additions & 23 deletions xpresso/routing/router.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import sys
import typing

if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol

import starlette.middleware
from starlette.routing import BaseRoute
from starlette.routing import Router as StarletteRouter
from starlette.types import ASGIApp, Receive, Scope, Send
from starlette.types import Receive, Scope, Send

from xpresso._utils.deprecation import not_supported
from xpresso.dependencies.models import Dependant
from xpresso.responses import Responses


class Router(StarletteRouter):
routes: typing.List[BaseRoute]
_app: ASGIApp
class _ASGIApp(Protocol):
def __call__(
self,
scope: Scope,
receive: Receive,
send: Send,
) -> typing.Awaitable[None]:
...


_MiddlewareIterator = typing.Iterable[
typing.Tuple[typing.Callable[..., _ASGIApp], typing.Mapping[str, typing.Any]]
]


class Router:
routes: typing.Sequence[BaseRoute]
lifespan: typing.Optional[
typing.Callable[..., typing.AsyncContextManager[None]]
] = None
dependencies: typing.Sequence[Dependant]
tags: typing.Sequence[str]
responses: Responses
include_in_schema: bool
_app: _ASGIApp

def __init__(
self,
Expand All @@ -25,26 +52,28 @@ def __init__(
typing.Callable[..., typing.AsyncContextManager[None]]
] = None,
redirect_slashes: bool = True,
default: typing.Optional[ASGIApp] = None,
dependencies: typing.Optional[typing.List[Dependant]] = None,
default: typing.Optional[_ASGIApp] = None,
dependencies: typing.Optional[typing.Sequence[Dependant]] = None,
tags: typing.Optional[typing.List[str]] = None,
responses: typing.Optional[Responses] = None,
include_in_schema: bool = True,
) -> None:
super().__init__( # type: ignore
routes=list(routes),
self.routes = list(routes)
self.lifespan = lifespan
self._router = StarletteRouter(
routes=self.routes,
redirect_slashes=redirect_slashes,
default=default, # type: ignore
lifespan=lifespan, # type: ignore
default=default, # type: ignore[arg-type]
lifespan=lifespan, # type: ignore[arg-type]
)
self.dependencies = list(dependencies or [])
self.tags = list(tags or [])
self.responses = dict(responses or {})
self.include_in_schema = include_in_schema
self._app = super().__call__ # type: ignore[assignment,misc]
self._app = self._router.__call__
if middleware is not None:
for cls, options in reversed(middleware): # type: ignore # for Pylance
self._app = cls(app=self._app, **options) # type: ignore[assignment,misc]
for cls, options in typing.cast(_MiddlewareIterator, reversed(middleware)):
self._app = cls(app=self._app, **options)

async def __call__(
self,
Expand All @@ -53,12 +82,3 @@ async def __call__(
send: Send,
) -> None:
await self._app(scope, receive, send) # type: ignore[arg-type,call-arg,misc]

mount = not_supported("mount")
host = not_supported("host")
add_route = not_supported("add_route")
add_websocket_route = not_supported("add_websocket_route")
route = not_supported("route")
websocket_route = not_supported("websocket_route")
add_event_handler = not_supported("add_event_handler")
on_event = not_supported("on_event")

0 comments on commit a4c4dbe

Please sign in to comment.