5
5
import re
6
6
import traceback
7
7
import urllib .parse
8
- from collections .abc import Coroutine , Iterable
8
+ from collections .abc import Iterable
9
9
from dataclasses import dataclass
10
10
from pathlib import Path
11
- from typing import Any , Callable
11
+ from typing import Any
12
12
13
13
import orjson
14
+ from asgiref import typing as asgi_types
14
15
from asgiref .compatibility import guarantee_single_callable
15
16
from servestatic import ServeStaticASGI
16
17
from typing_extensions import Unpack
@@ -33,7 +34,7 @@ class ReactPyMiddleware:
33
34
34
35
def __init__ (
35
36
self ,
36
- app : Callable [..., Coroutine ] ,
37
+ app : asgi_types . ASGIApplication ,
37
38
root_components : Iterable [str ],
38
39
** settings : Unpack [ReactPyConfig ],
39
40
) -> None :
@@ -61,7 +62,7 @@ def __init__(
61
62
self .static_pattern = re .compile (f"^{ self .static_path } .*" )
62
63
63
64
# Component attributes
64
- self .user_app = guarantee_single_callable (app )
65
+ self .user_app : asgi_types . ASGI3Application = guarantee_single_callable (app ) # type: ignore
65
66
self .root_components = import_components (root_components )
66
67
67
68
# Directory attributes
@@ -84,9 +85,9 @@ def __init__(
84
85
85
86
async def __call__ (
86
87
self ,
87
- scope : dict [ str , Any ] ,
88
- receive : Callable [..., Coroutine ] ,
89
- send : Callable [..., Coroutine ] ,
88
+ scope : asgi_types . Scope ,
89
+ receive : asgi_types . ASGIReceiveCallable ,
90
+ send : asgi_types . ASGISendCallable ,
90
91
) -> None :
91
92
"""The ASGI entrypoint that determines whether ReactPy should route the
92
93
request to ourselves or to the user application."""
@@ -105,13 +106,13 @@ async def __call__(
105
106
# Serve the user's application
106
107
await self .user_app (scope , receive , send )
107
108
108
- def match_dispatch_path (self , scope : dict ) -> bool :
109
+ def match_dispatch_path (self , scope : asgi_types . WebSocketScope ) -> bool :
109
110
return bool (re .match (self .dispatcher_pattern , scope ["path" ]))
110
111
111
- def match_static_path (self , scope : dict ) -> bool :
112
+ def match_static_path (self , scope : asgi_types . HTTPScope ) -> bool :
112
113
return bool (re .match (self .static_pattern , scope ["path" ]))
113
114
114
- def match_web_modules_path (self , scope : dict ) -> bool :
115
+ def match_web_modules_path (self , scope : asgi_types . HTTPScope ) -> bool :
115
116
return bool (re .match (self .js_modules_pattern , scope ["path" ]))
116
117
117
118
@@ -121,19 +122,21 @@ class ComponentDispatchApp:
121
122
122
123
async def __call__ (
123
124
self ,
124
- scope : dict [ str , Any ] ,
125
- receive : Callable [..., Coroutine ] ,
126
- send : Callable [..., Coroutine ] ,
125
+ scope : asgi_types . WebSocketScope ,
126
+ receive : asgi_types . ASGIReceiveCallable ,
127
+ send : asgi_types . ASGISendCallable ,
127
128
) -> None :
128
129
"""ASGI app for rendering ReactPy Python components."""
129
- dispatcher : asyncio .Task | None = None
130
- recv_queue : asyncio .Queue = asyncio .Queue ()
130
+ dispatcher : asyncio .Task [ Any ] | None = None
131
+ recv_queue : asyncio .Queue [ dict [ str , Any ]] = asyncio .Queue ()
131
132
132
133
# Start a loop that handles ASGI websocket events
133
134
while True :
134
135
event = await receive ()
135
136
if event ["type" ] == "websocket.connect" :
136
- await send ({"type" : "websocket.accept" })
137
+ await send (
138
+ {"type" : "websocket.accept" , "subprotocol" : None , "headers" : []}
139
+ )
137
140
dispatcher = asyncio .create_task (
138
141
self .run_dispatcher (scope , receive , send , recv_queue )
139
142
)
@@ -143,16 +146,16 @@ async def __call__(
143
146
dispatcher .cancel ()
144
147
break
145
148
146
- elif event ["type" ] == "websocket.receive" :
149
+ elif event ["type" ] == "websocket.receive" and event [ "text" ] :
147
150
queue_put_func = recv_queue .put (orjson .loads (event ["text" ]))
148
151
await queue_put_func
149
152
150
153
async def run_dispatcher (
151
154
self ,
152
- scope : dict [ str , Any ] ,
153
- receive : Callable [..., Coroutine ] ,
154
- send : Callable [..., Coroutine ] ,
155
- recv_queue : asyncio .Queue ,
155
+ scope : asgi_types . WebSocketScope ,
156
+ receive : asgi_types . ASGIReceiveCallable ,
157
+ send : asgi_types . ASGISendCallable ,
158
+ recv_queue : asyncio .Queue [ dict [ str , Any ]] ,
156
159
) -> None :
157
160
"""Asyncio background task that renders and transmits layout updates of ReactPy components."""
158
161
try :
@@ -187,11 +190,15 @@ async def run_dispatcher(
187
190
188
191
# Start the ReactPy component rendering loop
189
192
await serve_layout (
190
- Layout (ConnectionContext (component (), value = connection )), # type: ignore
193
+ Layout (ConnectionContext (component (), value = connection )),
191
194
lambda msg : send (
192
- {"type" : "websocket.send" , "text" : orjson .dumps (msg ).decode ()}
195
+ {
196
+ "type" : "websocket.send" ,
197
+ "text" : orjson .dumps (msg ).decode (),
198
+ "bytes" : None ,
199
+ }
193
200
),
194
- recv_queue .get ,
201
+ recv_queue .get , # type: ignore
195
202
)
196
203
197
204
# Manually log exceptions since this function is running in a separate asyncio task.
@@ -206,9 +213,9 @@ class StaticFileApp:
206
213
207
214
async def __call__ (
208
215
self ,
209
- scope : dict [ str , Any ] ,
210
- receive : Callable [..., Coroutine ] ,
211
- send : Callable [..., Coroutine ] ,
216
+ scope : asgi_types . HTTPScope ,
217
+ receive : asgi_types . ASGIReceiveCallable ,
218
+ send : asgi_types . ASGISendCallable ,
212
219
) -> None :
213
220
"""ASGI app for ReactPy static files."""
214
221
if not self ._static_file_server :
@@ -218,7 +225,7 @@ async def __call__(
218
225
prefix = self .parent .static_path ,
219
226
)
220
227
221
- return await self ._static_file_server (scope , receive , send )
228
+ await self ._static_file_server (scope , receive , send )
222
229
223
230
224
231
@dataclass
@@ -228,9 +235,9 @@ class WebModuleApp:
228
235
229
236
async def __call__ (
230
237
self ,
231
- scope : dict [ str , Any ] ,
232
- receive : Callable [..., Coroutine ] ,
233
- send : Callable [..., Coroutine ] ,
238
+ scope : asgi_types . HTTPScope ,
239
+ receive : asgi_types . ASGIReceiveCallable ,
240
+ send : asgi_types . ASGISendCallable ,
234
241
) -> None :
235
242
"""ASGI app for ReactPy web modules."""
236
243
if not self ._static_file_server :
@@ -241,4 +248,4 @@ async def __call__(
241
248
autorefresh = True ,
242
249
)
243
250
244
- return await self ._static_file_server (scope , receive , send )
251
+ await self ._static_file_server (scope , receive , send )
0 commit comments