1313import orjson
1414from asgiref .compatibility import guarantee_single_callable
1515from servestatic import ServeStaticASGI
16+ from typing_extensions import Unpack
1617
17- from reactpy . asgi . utils import check_path , import_components
18- from reactpy .config import REACTPY_WEB_MODULES_DIR
18+ from reactpy import config
19+ from reactpy .asgi . utils import check_path , import_components , process_settings
1920from reactpy .core .hooks import ConnectionContext
2021from reactpy .core .layout import Layout
2122from reactpy .core .serve import serve_layout
22- from reactpy .types import Connection , Location , RootComponentConstructor
23+ from reactpy .types import Connection , Location , ReactPyConfig , RootComponentConstructor
2324
2425_logger = logging .getLogger (__name__ )
2526
@@ -34,14 +35,15 @@ def __init__(
3435 self ,
3536 app : Callable [..., Coroutine ],
3637 root_components : Iterable [str ],
37- * ,
38- # TODO: Add a setting attribute to this class. Or maybe just put a shit ton of kwargs here. Or add a **kwargs that resolves to a TypedDict?
39- path_prefix : str = "/reactpy/" ,
40- web_modules_dir : Path | None = None ,
38+ ** settings : Unpack [ReactPyConfig ],
4139 ) -> None :
42- """Configure the ASGI app. Anything initialized in this method will be shared across all future requests."""
40+ """Configure the ASGI app. Anything initialized in this method will be shared across all future requests.
41+ TODO: Add types in docstring"""
42+ # Process global settings
43+ process_settings (settings )
44+
4345 # URL path attributes
44- self .path_prefix = path_prefix
46+ self .path_prefix = config . REACTPY_PATH_PREFIX . current
4547 self .dispatcher_path = self .path_prefix
4648 self .web_modules_path = f"{ self .path_prefix } modules/"
4749 self .static_path = f"{ self .path_prefix } static/"
@@ -56,20 +58,22 @@ def __init__(
5658 self .root_components = import_components (root_components )
5759
5860 # Directory attributes
59- self .web_modules_dir = web_modules_dir or REACTPY_WEB_MODULES_DIR .current
61+ self .web_modules_dir = config . REACTPY_WEB_MODULES_DIR .current
6062 self .static_dir = Path (__file__ ).parent .parent / "static"
61- if self .web_modules_dir != REACTPY_WEB_MODULES_DIR .current :
62- REACTPY_WEB_MODULES_DIR .set_current (self .web_modules_dir )
6363
6464 # Sub-applications
6565 self .component_dispatch_app = ComponentDispatchApp (parent = self )
6666 self .static_file_app = StaticFileApp (parent = self )
6767 self .web_modules_app = WebModuleApp (parent = self )
6868
6969 # Validate the configuration
70- reason = check_path (path_prefix )
70+ reason = check_path (self . path_prefix )
7171 if reason :
7272 raise ValueError (f"Invalid `path_prefix`. { reason } " )
73+ if not self .web_modules_dir .exists ():
74+ raise ValueError (
75+ f"Web modules directory { self .web_modules_dir } does not exist."
76+ )
7377
7478 async def __call__ (
7579 self ,
@@ -127,12 +131,12 @@ async def __call__(
127131 self .run_dispatcher (scope , receive , send , recv_queue )
128132 )
129133
130- if event ["type" ] == "websocket.disconnect" :
134+ elif event ["type" ] == "websocket.disconnect" :
131135 if dispatcher :
132136 dispatcher .cancel ()
133137 break
134138
135- if event ["type" ] == "websocket.receive" :
139+ elif event ["type" ] == "websocket.receive" :
136140 queue_put_func = recv_queue .put (orjson .loads (event ["text" ]))
137141 await queue_put_func
138142
@@ -143,8 +147,9 @@ async def run_dispatcher(
143147 send : Callable [..., Coroutine ],
144148 recv_queue : asyncio .Queue ,
145149 ) -> None :
146- # Get the component from the URL.
150+ """Asyncio background task that renders and transmits layout updates of ReactPy components."""
147151 try :
152+ # Determine component to serve by analyzing the URL and/or class parameters.
148153 if self .parent .multiple_root_components :
149154 url_match = re .match (self .parent .dispatcher_pattern , scope ["path" ])
150155 if not url_match :
@@ -160,42 +165,38 @@ async def run_dispatcher(
160165 else :
161166 raise RuntimeError ("No root component provided." )
162167
163- # TODO: Get HTTP URL from `http_pathname` and `http_query_string`
164- parsed_url = urllib .parse .urlparse (scope ["path" ])
165- pathname = parsed_url .path
166- query_string = f"?{ parsed_url .query } " if parsed_url .query else ""
168+ # Create a connection object by analyzing the websocket's query string.
169+ ws_query_string = urllib .parse .parse_qs (
170+ scope ["query_string" ].decode (), strict_parsing = True
171+ )
172+ connection = Connection (
173+ scope = scope ,
174+ location = Location (
175+ pathname = ws_query_string .get ("http_pathname" , ["" ])[0 ],
176+ query_string = ws_query_string .get ("http_search" , ["" ])[0 ],
177+ ),
178+ carrier = self ,
179+ )
167180
181+ # Start the ReactPy component rendering loop
168182 await serve_layout (
169- Layout ( # type: ignore
170- ConnectionContext (
171- component (),
172- value = Connection (
173- scope = scope ,
174- location = Location (
175- pathname = pathname , query_string = query_string
176- ),
177- carrier = {
178- "scope" : scope ,
179- "send" : send ,
180- "receive" : receive ,
181- },
182- ),
183- )
184- ),
185- self .send_json (send ),
183+ Layout (ConnectionContext (component (), value = connection )), # type: ignore
184+ self ._send_json (send ),
186185 recv_queue .get ,
187186 )
187+
188+ # Manually log exceptions since this function is running in a separate asyncio task.
188189 except Exception as error :
189190 await asyncio .to_thread (_logger .error , f"{ error } \n { traceback .format_exc ()} " )
190191
191192 @staticmethod
192- def send_json (send : Callable ) -> Callable [..., Coroutine ]:
193+ def _send_json (send : Callable ) -> Callable [..., Coroutine ]:
193194 """Use orjson to send JSON over an ASGI websocket."""
194195
195- async def _send_json (value : Any ) -> None :
196+ async def _send (value : Any ) -> None :
196197 await send ({"type" : "websocket.send" , "text" : orjson .dumps (value ).decode ()})
197198
198- return _send_json
199+ return _send
199200
200201
201202@dataclass
@@ -210,14 +211,6 @@ async def __call__(
210211 send : Callable [..., Coroutine ],
211212 ) -> None :
212213 """ASGI app for ReactPy static files."""
213- # If no static directory is configured, serve the user's application
214- if not self .parent .static_dir :
215- await asyncio .to_thread (
216- _logger .info ,
217- "Tried to serve static file without a configured directory." ,
218- )
219- return await self .parent .user_app (scope , receive , send )
220-
221214 if not self ._static_file_server :
222215 self ._static_file_server = ServeStaticASGI (
223216 self .parent .user_app ,
@@ -240,13 +233,6 @@ async def __call__(
240233 send : Callable [..., Coroutine ],
241234 ) -> None :
242235 """ASGI app for ReactPy web modules."""
243- if not self .parent .web_modules_dir :
244- await asyncio .to_thread (
245- _logger .info ,
246- "Tried to serve web module without a configured directory." ,
247- )
248- return await self .parent .user_app (scope , receive , send )
249-
250236 if not self ._static_file_server :
251237 self ._static_file_server = ServeStaticASGI (
252238 self .parent .user_app ,
0 commit comments