Skip to content

Commit 303d526

Browse files
committed
Overhaul ASGI type hints
1 parent babc2de commit 303d526

File tree

6 files changed

+146
-119
lines changed

6 files changed

+146
-119
lines changed

src/reactpy/core/hooks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,9 @@ def use_connection() -> Connection[Any]:
338338
return conn
339339

340340

341-
def use_scope() -> asgi_types.HTTPScope | asgi_types.WebSocketScope:
341+
def use_scope() -> dict[str, Any] | asgi_types.HTTPScope | asgi_types.WebSocketScope:
342342
"""Get the current :class:`~reactpy.types.Connection`'s scope."""
343-
return use_connection().scope # type: ignore
343+
return use_connection().scope
344344

345345

346346
def use_location() -> Location:

src/reactpy/executors/asgi/middleware.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import orjson
1414
from asgi_tools import ResponseText, ResponseWebSocket
15-
from asgiref import typing as asgi_types
1615
from asgiref.compatibility import guarantee_single_callable
1716
from servestatic import ServeStaticASGI
1817
from typing_extensions import Unpack
@@ -23,10 +22,18 @@
2322
from reactpy.core.serve import serve_layout
2423
from reactpy.executors.asgi.types import (
2524
AsgiApp,
26-
AsgiHttpApp,
27-
AsgiLifespanApp,
28-
AsgiWebsocketApp,
25+
AsgiHttpReceive,
26+
AsgiHttpScope,
27+
AsgiHttpSend,
28+
AsgiReceive,
29+
AsgiScope,
30+
AsgiSend,
31+
AsgiV3App,
32+
AsgiV3HttpApp,
33+
AsgiV3LifespanApp,
34+
AsgiV3WebsocketApp,
2935
AsgiWebsocketReceive,
36+
AsgiWebsocketScope,
3037
AsgiWebsocketSend,
3138
)
3239
from reactpy.executors.utils import check_path, import_components, process_settings
@@ -42,7 +49,7 @@ class ReactPyMiddleware:
4249

4350
def __init__(
4451
self,
45-
app: asgi_types.ASGIApplication,
52+
app: AsgiApp,
4653
root_components: Iterable[str],
4754
**settings: Unpack[ReactPyConfig],
4855
) -> None:
@@ -80,12 +87,12 @@ def __init__(
8087
)
8188

8289
# User defined ASGI apps
83-
self.extra_http_routes: dict[str, AsgiHttpApp] = {}
84-
self.extra_ws_routes: dict[str, AsgiWebsocketApp] = {}
85-
self.extra_lifespan_app: AsgiLifespanApp | None = None
90+
self.extra_http_routes: dict[str, AsgiV3HttpApp] = {}
91+
self.extra_ws_routes: dict[str, AsgiV3WebsocketApp] = {}
92+
self.extra_lifespan_app: AsgiV3LifespanApp | None = None
8693

8794
# Component attributes
88-
self.asgi_app: asgi_types.ASGI3Application = guarantee_single_callable(app) # type: ignore
95+
self.asgi_app: AsgiV3App = guarantee_single_callable(app) # type: ignore
8996
self.root_components = import_components(root_components)
9097

9198
# Directory attributes
@@ -98,10 +105,7 @@ def __init__(
98105
self.web_modules_app = WebModuleApp(parent=self)
99106

100107
async def __call__(
101-
self,
102-
scope: asgi_types.Scope,
103-
receive: asgi_types.ASGIReceiveCallable,
104-
send: asgi_types.ASGISendCallable,
108+
self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend
105109
) -> None:
106110
"""The ASGI entrypoint that determines whether ReactPy should route the
107111
request to ourselves or to the user application."""
@@ -125,16 +129,16 @@ async def __call__(
125129
# Serve the user's application
126130
await self.asgi_app(scope, receive, send)
127131

128-
def match_dispatch_path(self, scope: asgi_types.WebSocketScope) -> bool:
132+
def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool:
129133
return bool(re.match(self.dispatcher_pattern, scope["path"]))
130134

131-
def match_static_path(self, scope: asgi_types.HTTPScope) -> bool:
135+
def match_static_path(self, scope: AsgiHttpScope) -> bool:
132136
return scope["path"].startswith(self.static_path)
133137

134-
def match_web_modules_path(self, scope: asgi_types.HTTPScope) -> bool:
138+
def match_web_modules_path(self, scope: AsgiHttpScope) -> bool:
135139
return scope["path"].startswith(self.web_modules_path)
136140

137-
def match_extra_paths(self, scope: asgi_types.Scope) -> AsgiApp | None:
141+
def match_extra_paths(self, scope: AsgiScope) -> AsgiApp | None:
138142
# Custom defined routes are unused by default to encourage users to handle
139143
# routing within their ASGI framework of choice.
140144
return None
@@ -146,13 +150,13 @@ class ComponentDispatchApp:
146150

147151
async def __call__(
148152
self,
149-
scope: asgi_types.WebSocketScope,
150-
receive: asgi_types.ASGIReceiveCallable,
151-
send: asgi_types.ASGISendCallable,
153+
scope: AsgiWebsocketScope,
154+
receive: AsgiWebsocketReceive,
155+
send: AsgiWebsocketSend,
152156
) -> None:
153157
"""ASGI app for rendering ReactPy Python components."""
154158
# Start a loop that handles ASGI websocket events
155-
async with ReactPyWebsocket(scope, receive, send, parent=self.parent) as ws: # type: ignore
159+
async with ReactPyWebsocket(scope, receive, send, parent=self.parent) as ws:
156160
while True:
157161
# Wait for the webserver to notify us of a new event
158162
event: dict[str, Any] = await ws.receive(raw=True) # type: ignore
@@ -175,7 +179,7 @@ async def __call__(
175179
class ReactPyWebsocket(ResponseWebSocket):
176180
def __init__(
177181
self,
178-
scope: asgi_types.WebSocketScope,
182+
scope: AsgiWebsocketScope,
179183
receive: AsgiWebsocketReceive,
180184
send: AsgiWebsocketSend,
181185
parent: ReactPyMiddleware,
@@ -250,10 +254,7 @@ class StaticFileApp:
250254
_static_file_server: ServeStaticASGI | None = None
251255

252256
async def __call__(
253-
self,
254-
scope: asgi_types.HTTPScope,
255-
receive: asgi_types.ASGIReceiveCallable,
256-
send: asgi_types.ASGISendCallable,
257+
self, scope: AsgiHttpScope, receive: AsgiHttpReceive, send: AsgiHttpSend
257258
) -> None:
258259
"""ASGI app for ReactPy static files."""
259260
if not self._static_file_server:
@@ -272,10 +273,7 @@ class WebModuleApp:
272273
_static_file_server: ServeStaticASGI | None = None
273274

274275
async def __call__(
275-
self,
276-
scope: asgi_types.HTTPScope,
277-
receive: asgi_types.ASGIReceiveCallable,
278-
send: asgi_types.ASGISendCallable,
276+
self, scope: AsgiHttpScope, receive: AsgiHttpReceive, send: AsgiHttpSend
279277
) -> None:
280278
"""ASGI app for ReactPy web modules."""
281279
if not self._static_file_server:
@@ -291,10 +289,7 @@ async def __call__(
291289

292290
class Error404App:
293291
async def __call__(
294-
self,
295-
scope: asgi_types.HTTPScope,
296-
receive: asgi_types.ASGIReceiveCallable,
297-
send: asgi_types.ASGISendCallable,
292+
self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend
298293
) -> None:
299294
response = ResponseText("Resource not found on this server.", status_code=404)
300295
await response(scope, receive, send) # type: ignore

src/reactpy/executors/asgi/pyscript.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
from pathlib import Path
1010
from typing import Any
1111

12-
from asgiref.typing import WebSocketScope
1312
from typing_extensions import Unpack
1413

1514
from reactpy import html
1615
from reactpy.executors.asgi.middleware import ReactPyMiddleware
1716
from reactpy.executors.asgi.standalone import ReactPy, ReactPyApp
17+
from reactpy.executors.asgi.types import AsgiWebsocketScope
1818
from reactpy.executors.utils import vdom_head_to_html
1919
from reactpy.pyscript.utils import pyscript_component_html, pyscript_setup_html
2020
from reactpy.types import ReactPyConfig, VdomDict
@@ -79,7 +79,9 @@ def __init__(
7979
self.html_head = html_head or html.head()
8080
self.html_lang = html_lang
8181

82-
def match_dispatch_path(self, scope: WebSocketScope) -> bool: # pragma: no cover
82+
def match_dispatch_path(
83+
self, scope: AsgiWebsocketScope
84+
) -> bool: # pragma: no cover
8385
"""We do not use a WebSocket dispatcher for Client-Side Rendering (CSR)."""
8486
return False
8587

src/reactpy/executors/asgi/standalone.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@
99
from typing import Callable, Literal, cast, overload
1010

1111
from asgi_tools import ResponseHTML
12-
from asgiref import typing as asgi_types
1312
from typing_extensions import Unpack
1413

1514
from reactpy import html
1615
from reactpy.executors.asgi.middleware import ReactPyMiddleware
1716
from reactpy.executors.asgi.types import (
1817
AsgiApp,
19-
AsgiHttpApp,
20-
AsgiLifespanApp,
21-
AsgiWebsocketApp,
18+
AsgiReceive,
19+
AsgiScope,
20+
AsgiSend,
21+
AsgiV3HttpApp,
22+
AsgiV3LifespanApp,
23+
AsgiV3WebsocketApp,
24+
AsgiWebsocketScope,
2225
)
2326
from reactpy.executors.utils import server_side_component_html, vdom_head_to_html
2427
from reactpy.pyscript.utils import pyscript_setup_html
@@ -77,11 +80,11 @@ def __init__(
7780
pyscript_head_vdom["tagName"] = ""
7881
self.html_head["children"].append(pyscript_head_vdom) # type: ignore
7982

80-
def match_dispatch_path(self, scope: asgi_types.WebSocketScope) -> bool:
83+
def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool:
8184
"""Method override to remove `dotted_path` from the dispatcher URL."""
8285
return str(scope["path"]) == self.dispatcher_path
8386

84-
def match_extra_paths(self, scope: asgi_types.Scope) -> AsgiApp | None:
87+
def match_extra_paths(self, scope: AsgiScope) -> AsgiApp | None:
8588
"""Method override to match user-provided HTTP/Websocket routes."""
8689
if scope["type"] == "lifespan":
8790
return self.extra_lifespan_app
@@ -106,22 +109,22 @@ def route(
106109
self,
107110
path: str,
108111
type: Literal["http"] = "http",
109-
) -> Callable[[AsgiHttpApp | str], AsgiApp]: ...
112+
) -> Callable[[AsgiV3HttpApp | str], AsgiApp]: ...
110113

111114
@overload
112115
def route(
113116
self,
114117
path: str,
115118
type: Literal["websocket"],
116-
) -> Callable[[AsgiWebsocketApp | str], AsgiApp]: ...
119+
) -> Callable[[AsgiV3WebsocketApp | str], AsgiApp]: ...
117120

118121
def route(
119122
self,
120123
path: str,
121124
type: Literal["http", "websocket"] = "http",
122125
) -> (
123-
Callable[[AsgiHttpApp | str], AsgiApp]
124-
| Callable[[AsgiWebsocketApp | str], AsgiApp]
126+
Callable[[AsgiV3HttpApp | str], AsgiApp]
127+
| Callable[[AsgiV3WebsocketApp | str], AsgiApp]
125128
):
126129
"""Interface that allows user to define their own HTTP/Websocket routes
127130
within the current ReactPy application.
@@ -142,15 +145,15 @@ def decorator(
142145

143146
asgi_app: AsgiApp = import_dotted_path(app) if isinstance(app, str) else app
144147
if type == "http":
145-
self.extra_http_routes[re_path] = cast(AsgiHttpApp, asgi_app)
148+
self.extra_http_routes[re_path] = cast(AsgiV3HttpApp, asgi_app)
146149
elif type == "websocket":
147-
self.extra_ws_routes[re_path] = cast(AsgiWebsocketApp, asgi_app)
150+
self.extra_ws_routes[re_path] = cast(AsgiV3WebsocketApp, asgi_app)
148151

149152
return asgi_app
150153

151154
return decorator
152155

153-
def lifespan(self, app: AsgiLifespanApp | str) -> None:
156+
def lifespan(self, app: AsgiV3LifespanApp | str) -> None:
154157
"""Interface that allows user to define their own lifespan app
155158
within the current ReactPy application.
156159
@@ -176,10 +179,7 @@ class ReactPyApp:
176179
_last_modified = ""
177180

178181
async def __call__(
179-
self,
180-
scope: asgi_types.Scope,
181-
receive: asgi_types.ASGIReceiveCallable,
182-
send: asgi_types.ASGISendCallable,
182+
self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend
183183
) -> None:
184184
if scope["type"] != "http": # pragma: no cover
185185
if scope["type"] != "lifespan":

0 commit comments

Comments
 (0)