Skip to content

Commit 44ebb66

Browse files
committed
implement connection context
1 parent ca6f1b5 commit 44ebb66

File tree

13 files changed

+99
-117
lines changed

13 files changed

+99
-117
lines changed

src/idom/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .core.vdom import vdom
1818
from .server.utils import run
1919
from .utils import Ref, html_to_vdom
20-
from .widgets import hotswap, multiview
20+
from .widgets import hotswap
2121

2222

2323
__author__ = "idom-team"
@@ -34,7 +34,6 @@
3434
"html",
3535
"Layout",
3636
"log",
37-
"multiview",
3837
"Ref",
3938
"run",
4039
"Stop",

src/idom/core/hooks.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,19 @@ def effect() -> None:
206206

207207
def create_context(
208208
default_value: _StateType, name: str | None = None
209-
) -> type[_Context[_StateType]]:
209+
) -> type[Context[_StateType]]:
210210
"""Return a new context type for use in :func:`use_context`"""
211211

212-
class Context(_Context[_StateType]):
212+
class _Context(Context[_StateType]):
213213
_default_value = default_value
214214

215215
if name is not None:
216-
Context.__name__ = name
216+
_Context.__name__ = name
217217

218-
return Context
218+
return _Context
219219

220220

221-
def use_context(context_type: type[_Context[_StateType]]) -> _StateType:
221+
def use_context(context_type: type[Context[_StateType]]) -> _StateType:
222222
"""Get the current value for the given context type.
223223
224224
See the full :ref:`Use Context` docs for more information.
@@ -228,7 +228,7 @@ def use_context(context_type: type[_Context[_StateType]]) -> _StateType:
228228
# that newly present current context. When we update it though, we don't need to
229229
# schedule a new render since we're already rending right now. Thus we can't do this
230230
# with use_state() since we'd incur an extra render when calling set_state.
231-
context_ref: Ref[_Context[_StateType] | None] = use_ref(None)
231+
context_ref: Ref[Context[_StateType] | None] = use_ref(None)
232232

233233
if context_ref.current is None:
234234
provided_context = context_type._current.get()
@@ -244,7 +244,7 @@ def use_context(context_type: type[_Context[_StateType]]) -> _StateType:
244244

245245
@use_effect
246246
def subscribe_to_context_change() -> Callable[[], None]:
247-
def set_context(new: _Context[_StateType]) -> None:
247+
def set_context(new: Context[_StateType]) -> None:
248248
# We don't need to check if `new is not context_ref.current` because we only
249249
# trigger this callback when the value of a context, and thus the context
250250
# itself changes. Therefore we can always schedule a render.
@@ -260,13 +260,13 @@ def set_context(new: _Context[_StateType]) -> None:
260260
_UNDEFINED: Any = object()
261261

262262

263-
class _Context(Generic[_StateType]):
263+
class Context(Generic[_StateType]):
264264

265265
# This should be _StateType instead of Any, but it can't due to this limitation:
266266
# https://github.com/python/mypy/issues/5144
267267
_default_value: ClassVar[Any]
268268

269-
_current: ClassVar[ThreadLocal[_Context[Any] | None]]
269+
_current: ClassVar[ThreadLocal[Context[Any] | None]]
270270

271271
def __init_subclass__(cls) -> None:
272272
# every context type tracks which of its instances are currently in use
@@ -281,7 +281,7 @@ def __init__(
281281
self.children = children
282282
self.value: _StateType = self._default_value if value is _UNDEFINED else value
283283
self.key = key
284-
self.subscribers: set[Callable[[_Context[_StateType]], None]] = set()
284+
self.subscribers: set[Callable[[Context[_StateType]], None]] = set()
285285
self.type = self.__class__
286286

287287
def render(self) -> VdomDict:
@@ -297,7 +297,7 @@ def reset_ctx() -> None:
297297

298298
return vdom("", *self.children)
299299

300-
def should_render(self, new: _Context[_StateType]) -> bool:
300+
def should_render(self, new: Context[_StateType]) -> bool:
301301
if self.value is not new.value:
302302
new.subscribers.update(self.subscribers)
303303
for set_context in self.subscribers:

src/idom/core/layout.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def _render_component(
193193

194194
if (
195195
old_state is not None
196+
and hasattr(old_state.model, "current")
196197
and old_state.is_component_state
197198
and not _check_should_render(
198199
old_state.life_cycle_state.component, component

src/idom/server/_conn.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from idom.core.hooks import create_context
6+
from idom.types import Context
7+
8+
9+
Connection: type[Context[Any | None]] = create_context(None, name="Connection")

src/idom/server/default.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ async def serve_development_app(
2828
)
2929

3030

31+
def use_connection() -> Any:
32+
return _default_implementation().use_connection()
33+
34+
3135
def _default_implementation() -> ServerImplementation[Any]:
3236
"""Get the first available server implementation"""
3337
global _DEFAULT_IMPLEMENTATION

src/idom/server/fastapi.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
_setup_options,
1212
_setup_single_view_dispatcher_route,
1313
serve_development_app,
14+
use_connection,
1415
)
1516

1617

17-
__all__ = "configure", "serve_development_app", "create_development_app"
18+
__all__ = (
19+
"configure",
20+
"serve_development_app",
21+
"create_development_app",
22+
"use_connection",
23+
)
1824

1925

2026
def configure(

src/idom/server/flask.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@
1010
from typing import Any, Callable, Dict, NamedTuple, Optional, Union, cast
1111
from urllib.parse import parse_qs as parse_query_string
1212

13-
from flask import Blueprint, Flask, redirect, request, send_from_directory, url_for
13+
from flask import (
14+
Blueprint,
15+
Flask,
16+
Request,
17+
copy_current_request_context,
18+
redirect,
19+
request,
20+
send_from_directory,
21+
url_for,
22+
)
1423
from flask_cors import CORS
1524
from flask_sockets import Sockets
1625
from gevent import pywsgi
@@ -20,10 +29,12 @@
2029

2130
import idom
2231
from idom.config import IDOM_WEB_MODULES_DIR
32+
from idom.core.hooks import use_context
2333
from idom.core.layout import LayoutEvent, LayoutUpdate
2434
from idom.core.serve import serve_json_patch
2535
from idom.core.types import ComponentType, RootComponentConstructor
2636

37+
from ._conn import Connection
2738
from .utils import CLIENT_BUILD_DIR
2839

2940

@@ -94,6 +105,13 @@ def run_server():
94105
raise RuntimeError("Failed to shutdown server.")
95106

96107

108+
def use_connection() -> Request:
109+
value = use_context(Connection)
110+
if value is None:
111+
raise RuntimeError("No established connection.")
112+
return value
113+
114+
97115
class Options(TypedDict, total=False):
98116
"""Render server config for :class:`FlaskRenderServer`"""
99117

@@ -175,14 +193,7 @@ def recv() -> Optional[LayoutEvent]:
175193
else:
176194
return None
177195

178-
dispatch_in_thread(constructor(**_get_query_params(ws)), send, recv)
179-
180-
181-
def _get_query_params(ws: WebSocket) -> Dict[str, Any]:
182-
return {
183-
k: v if len(v) > 1 else v[0]
184-
for k, v in parse_query_string(ws.environ["QUERY_STRING"]).items()
185-
}
196+
dispatch_in_thread(constructor(), send, recv)
186197

187198

188199
def dispatch_in_thread(
@@ -193,6 +204,7 @@ def dispatch_in_thread(
193204
dispatch_thread_info_created = ThreadEvent()
194205
dispatch_thread_info_ref: idom.Ref[Optional[_DispatcherThreadInfo]] = idom.Ref(None)
195206

207+
@copy_current_request_context
196208
def run_dispatcher() -> None:
197209
loop = asyncio.new_event_loop()
198210
asyncio.set_event_loop(loop)
@@ -207,7 +219,9 @@ async def recv_coro() -> Any:
207219
return await async_recv_queue.get()
208220

209221
async def main() -> None:
210-
await serve_json_patch(idom.Layout(component), send_coro, recv_coro)
222+
await serve_json_patch(
223+
idom.Layout(Connection(component, value=request)), send_coro, recv_coro
224+
)
211225

212226
main_future = asyncio.ensure_future(main())
213227

src/idom/server/sanic.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from idom.core.types import RootComponentConstructor
2424

25+
from ._conn import Connection
2526
from .utils import CLIENT_BUILD_DIR
2627

2728

@@ -63,6 +64,13 @@ async def serve_development_app(
6364
app.stop()
6465

6566

67+
def use_connection() -> request.Request:
68+
value = use_connection(Connection)
69+
if value is None:
70+
raise RuntimeError("No established connection.")
71+
return value
72+
73+
6674
class Options(TypedDict, total=False):
6775
"""Options for :class:`SanicRenderServer`"""
6876

@@ -122,7 +130,11 @@ async def model_stream(
122130
) -> None:
123131
send, recv = _make_send_recv_callbacks(socket)
124132
component_params = {k: request.args.get(k) for k in request.args}
125-
await serve_json_patch(Layout(constructor(**component_params)), send, recv)
133+
await serve_json_patch(
134+
Layout(Connection(constructor(**component_params), value=request)),
135+
send,
136+
recv,
137+
)
126138

127139

128140
def _make_send_recv_callbacks(

src/idom/server/starlette.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from uvicorn.server import Server as UvicornServer
1818

1919
from idom.config import IDOM_WEB_MODULES_DIR
20+
from idom.core.hooks import use_context
2021
from idom.core.layout import Layout, LayoutEvent
2122
from idom.core.serve import (
2223
RecvCoroutine,
@@ -27,6 +28,7 @@
2728
from idom.core.types import RootComponentConstructor
2829

2930
from ._asgi import serve_development_asgi
31+
from ._conn import Connection
3032
from .utils import CLIENT_BUILD_DIR
3133

3234

@@ -67,6 +69,13 @@ async def serve_development_app(
6769
await serve_development_asgi(app, host, port, started)
6870

6971

72+
def use_connection() -> WebSocket:
73+
value = use_context(Connection)
74+
if value is None:
75+
raise RuntimeError("No established connection.")
76+
return value
77+
78+
7079
class Options(TypedDict, total=False):
7180
"""Optionsuration options for :class:`StarletteRenderServer`"""
7281

@@ -145,7 +154,9 @@ async def model_stream(socket: WebSocket) -> None:
145154
send, recv = _make_send_recv_callbacks(socket)
146155
try:
147156
await serve_json_patch(
148-
Layout(constructor(**dict(socket.query_params))), send, recv
157+
Layout(Connection(constructor(), value=socket)),
158+
send,
159+
recv,
149160
)
150161
except WebSocketDisconnect as error:
151162
logger.info(f"WebSocket disconnect: {error.code}")

src/idom/server/tornado.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
from urllib.parse import urljoin
99

1010
from tornado.httpserver import HTTPServer
11+
from tornado.httputil import HTTPServerRequest
1112
from tornado.platform.asyncio import AsyncIOMainLoop
1213
from tornado.web import Application, RedirectHandler, RequestHandler, StaticFileHandler
1314
from tornado.websocket import WebSocketHandler
1415
from typing_extensions import TypedDict
1516

1617
from idom.config import IDOM_WEB_MODULES_DIR
18+
from idom.core.hooks import use_context
1719
from idom.core.layout import Layout, LayoutEvent
1820
from idom.core.serve import VdomJsonPatch, serve_json_patch
1921
from idom.core.types import ComponentConstructor
2022

23+
from ._conn import Connection
2124
from .utils import CLIENT_BUILD_DIR
2225

2326

@@ -69,6 +72,13 @@ async def serve_development_app(
6972
await server.close_all_connections()
7073

7174

75+
def use_connection() -> HTTPServerRequest:
76+
value = use_context(Connection)
77+
if value is None:
78+
raise RuntimeError("No established connection.")
79+
return value
80+
81+
7282
class Options(TypedDict, total=False):
7383
"""Render server options for :class:`TornadoRenderServer` subclasses"""
7484

@@ -160,7 +170,7 @@ async def recv() -> LayoutEvent:
160170
self._message_queue = message_queue
161171
self._dispatch_future = asyncio.ensure_future(
162172
serve_json_patch(
163-
Layout(self._component_constructor(**query_params)),
173+
Layout(Connection(self._component_constructor(), value=self.request)),
164174
send,
165175
recv,
166176
)

0 commit comments

Comments
 (0)