Skip to content

Commit fc3f28c

Browse files
committed
add use_connection hook
1 parent 50e42c0 commit fc3f28c

File tree

11 files changed

+246
-178
lines changed

11 files changed

+246
-178
lines changed

src/idom/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from . import backend, config, html, logging, sample, types, web
2+
from .backend.hooks import use_connection, use_location, use_scope
23
from .backend.utils import run
34
from .core import hooks
45
from .core.component import component
@@ -25,6 +26,7 @@
2526
__version__ = "0.40.2" # DO NOT MODIFY
2627

2728
__all__ = [
29+
"backend",
2830
"component",
2931
"config",
3032
"create_context",
@@ -38,16 +40,18 @@
3840
"Ref",
3941
"run",
4042
"sample",
41-
"backend",
4243
"Stop",
4344
"types",
4445
"use_callback",
46+
"use_connection",
4547
"use_context",
4648
"use_debug_value",
4749
"use_effect",
50+
"use_location",
4851
"use_memo",
4952
"use_reducer",
5053
"use_ref",
54+
"use_scope",
5155
"use_state",
5256
"vdom",
5357
"web",

src/idom/backend/default.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from idom.types import RootComponentConstructor
77

8-
from .types import BackendImplementation, Location
8+
from .types import BackendImplementation
99
from .utils import all_implementations
1010

1111

@@ -35,16 +35,6 @@ async def serve_development_app(
3535
)
3636

3737

38-
def use_scope() -> Any:
39-
"""Return the current ASGI/WSGI scope"""
40-
return _default_implementation().use_scope()
41-
42-
43-
def use_location() -> Location:
44-
"""Return the current route as a string"""
45-
return _default_implementation().use_location()
46-
47-
4838
_DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None
4939

5040

src/idom/backend/fastapi.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,10 @@
88
serve_development_app = starlette.serve_development_app
99
"""Alias for :func:`idom.backend.starlette.serve_development_app`"""
1010

11-
# see: https://github.com/idom-team/flake8-idom-hooks/issues/12
12-
use_location = starlette.use_location # noqa: ROH101
11+
use_connection = starlette.use_connection
1312
"""Alias for :func:`idom.backend.starlette.use_location`"""
1413

15-
# see: https://github.com/idom-team/flake8-idom-hooks/issues/12
16-
use_scope = starlette.use_scope # noqa: ROH101
17-
"""Alias for :func:`idom.backend.starlette.use_scope`"""
18-
19-
# see: https://github.com/idom-team/flake8-idom-hooks/issues/12
20-
use_websocket = starlette.use_websocket # noqa: ROH101
14+
use_websocket = starlette.use_websocket
2115
"""Alias for :func:`idom.backend.starlette.use_websocket`"""
2216

2317
Options = starlette.Options

src/idom/backend/flask.py

+38-38
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
from flask_cors import CORS
2323
from flask_sock import Sock
2424
from simple_websocket import Server as WebSocket
25+
from werkzeug.local import LocalProxy
2526
from werkzeug.serving import BaseWSGIServer, make_server
2627

2728
import idom
28-
from idom.backend.types import Location
29-
from idom.core.hooks import Context, create_context, use_context
29+
from idom.backend.hooks import ConnectionContext
30+
from idom.backend.hooks import use_connection as _use_connection
31+
from idom.backend.types import Connection, Location
3032
from idom.core.layout import LayoutEvent, LayoutUpdate
3133
from idom.core.serve import serve_json_patch
3234
from idom.core.types import ComponentType, RootComponentConstructor
@@ -37,8 +39,6 @@
3739

3840
logger = logging.getLogger(__name__)
3941

40-
ConnectionContext: Context[Connection | None] = create_context(None)
41-
4242

4343
def configure(
4444
app: Flask, component: RootComponentConstructor, options: Options | None = None
@@ -107,45 +107,25 @@ def run_server() -> None:
107107
raise RuntimeError("Failed to shutdown server.")
108108

109109

110-
def use_location() -> Location:
111-
"""Get the current route as a string"""
112-
conn = use_connection()
113-
search = conn.request.query_string.decode()
114-
return Location(pathname="/" + conn.path, search="?" + search if search else "")
115-
116-
117-
def use_scope() -> dict[str, Any]:
118-
"""Get the current WSGI environment"""
119-
return use_request().environ
110+
def use_websocket() -> WebSocket:
111+
"""A handle to the current websocket"""
112+
return use_connection().carrier.websocket
120113

121114

122-
def use_request() -> Request:
115+
def use_request() -> LocalProxy[Request]:
123116
"""Get the current ``Request``"""
124-
return use_connection().request
117+
return use_connection().carrier.request
125118

126119

127-
def use_connection() -> Connection:
120+
def use_connection() -> Connection[_FlaskCarrier]:
128121
"""Get the current :class:`Connection`"""
129-
connection = use_context(ConnectionContext)
130-
if connection is None:
131-
raise RuntimeError( # pragma: no cover
132-
"No connection. Are you running with a Flask server?"
122+
conn = _use_connection()
123+
if not isinstance(conn.carrier, _FlaskCarrier):
124+
raise TypeError( # pragma: no cover
125+
f"Connection has unexpected carrier {conn.carrier}. "
126+
"Are you running with a Flask server?"
133127
)
134-
return connection
135-
136-
137-
@dataclass
138-
class Connection:
139-
"""A simple wrapper for holding connection information"""
140-
141-
request: Request
142-
"""The current request object"""
143-
144-
websocket: WebSocket
145-
"""A handle to the current websocket"""
146-
147-
path: str
148-
"""The current path being served"""
128+
return conn
149129

150130

151131
@dataclass
@@ -230,11 +210,20 @@ async def recv_coro() -> Any:
230210
return await async_recv_queue.get()
231211

232212
async def main() -> None:
213+
search = request.query_string.decode()
233214
await serve_json_patch(
234215
idom.Layout(
235216
ConnectionContext(
236-
component, value=Connection(request, websocket, path)
237-
)
217+
component,
218+
value=Connection(
219+
scope=request.environ,
220+
location=Location(
221+
pathname=f"/{path}",
222+
search=f"?{search}" if search else "",
223+
),
224+
carrier=_FlaskCarrier(request, websocket),
225+
),
226+
),
238227
),
239228
send_coro,
240229
recv_coro,
@@ -283,3 +272,14 @@ class _DispatcherThreadInfo(NamedTuple):
283272
dispatch_future: "asyncio.Future[Any]"
284273
thread_send_queue: "ThreadQueue[LayoutUpdate]"
285274
async_recv_queue: "AsyncQueue[LayoutEvent]"
275+
276+
277+
@dataclass
278+
class _FlaskCarrier:
279+
"""A simple wrapper for holding a Flask request and WebSocket"""
280+
281+
request: Request
282+
"""The current request object"""
283+
284+
websocket: WebSocket
285+
"""A handle to the current websocket"""

src/idom/backend/hooks.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any, MutableMapping
2+
3+
from idom.core.hooks import Context, create_context, use_context
4+
5+
from .types import Connection, Location
6+
7+
8+
# backend implementations should establish this context at the root of an app
9+
ConnectionContext: Context[Connection[Any] | None] = create_context(None)
10+
11+
12+
def use_connection() -> Connection[Any]:
13+
conn = use_context(ConnectionContext)
14+
if conn is None:
15+
raise RuntimeError("No backend established a connection.") # pragma: no cover
16+
return conn
17+
18+
19+
def use_scope() -> MutableMapping[str, Any]:
20+
return use_connection().scope
21+
22+
23+
def use_location() -> Location:
24+
return use_connection().location

src/idom/backend/sanic.py

+57-48
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@
44
import json
55
import logging
66
from dataclasses import dataclass
7-
from typing import Any, Dict, Tuple, Union
7+
from typing import Any, Dict, MutableMapping, Tuple, Union
88
from urllib import parse as urllib_parse
99
from uuid import uuid4
1010

1111
from sanic import Blueprint, Sanic, request, response
1212
from sanic.config import Config
1313
from sanic.models.asgi import ASGIScope
14+
from sanic.server.websockets.connection import WebSocketConnection
1415
from sanic_cors import CORS
15-
from websockets.legacy.protocol import WebSocketCommonProtocol
1616

17-
from idom.backend.types import Location
18-
from idom.core.hooks import Context, create_context, use_context
17+
from idom.backend.types import Connection, Location
1918
from idom.core.layout import Layout, LayoutEvent
2019
from idom.core.serve import (
2120
RecvCoroutine,
@@ -27,13 +26,13 @@
2726
from idom.core.types import RootComponentConstructor
2827

2928
from ._asgi import serve_development_asgi
29+
from .hooks import ConnectionContext
30+
from .hooks import use_connection as _use_connection
3031
from .utils import safe_client_build_dir_path, safe_web_modules_dir_path
3132

3233

3334
logger = logging.getLogger(__name__)
3435

35-
ConnectionContext: Context[Connection | None] = create_context(None)
36-
3736

3837
def configure(
3938
app: Sanic, component: RootComponentConstructor, options: Options | None = None
@@ -65,50 +64,25 @@ async def serve_development_app(
6564
await serve_development_asgi(app, host, port, started)
6665

6766

68-
def use_location() -> Location:
69-
"""Get the current route as a string"""
70-
conn = use_connection()
71-
search = conn.request.query_string
72-
return Location(pathname="/" + conn.path, search="?" + search if search else "")
73-
74-
75-
def use_scope() -> ASGIScope:
76-
"""Get the current ASGI scope"""
77-
app = use_request().app
78-
try:
79-
asgi_app = app._asgi_app
80-
except AttributeError: # pragma: no cover
81-
raise RuntimeError("No scope. Sanic may not be running with an ASGI server")
82-
return asgi_app.transport.scope
83-
84-
8567
def use_request() -> request.Request:
8668
"""Get the current ``Request``"""
87-
return use_connection().request
88-
89-
90-
def use_connection() -> Connection:
91-
"""Get the current :class:`Connection`"""
92-
connection = use_context(ConnectionContext)
93-
if connection is None:
94-
raise RuntimeError( # pragma: no cover
95-
"No connection. Are you running with a Sanic server?"
96-
)
97-
return connection
69+
return use_connection().carrier.request
9870

9971

100-
@dataclass
101-
class Connection:
102-
"""A simple wrapper for holding connection information"""
72+
def use_websocket() -> WebSocketConnection:
73+
"""Get the current websocket"""
74+
return use_connection().carrier.websocket
10375

104-
request: request.Request
105-
"""The current request object"""
10676

107-
websocket: WebSocketCommonProtocol
108-
"""A handle to the current websocket"""
109-
110-
path: str
111-
"""The current path being served"""
77+
def use_connection() -> Connection[_SanicCarrier]:
78+
"""Get the current :class:`Connection`"""
79+
conn = _use_connection()
80+
if not isinstance(conn.carrier, _SanicCarrier):
81+
raise TypeError( # pragma: no cover
82+
f"Connection has unexpected carrier {conn.carrier}. "
83+
"Are you running with a Sanic server?"
84+
)
85+
return conn
11286

11387

11488
@dataclass
@@ -165,12 +139,36 @@ def _setup_single_view_dispatcher_route(
165139
blueprint: Blueprint, constructor: RootComponentConstructor
166140
) -> None:
167141
async def model_stream(
168-
request: request.Request, socket: WebSocketCommonProtocol, path: str = ""
142+
request: request.Request, socket: WebSocketConnection, path: str = ""
169143
) -> None:
144+
app = request.app
145+
try:
146+
asgi_app = app._asgi_app
147+
except AttributeError: # pragma: no cover
148+
logger.warning("No scope. Sanic may not be running with an ASGI server")
149+
scope: MutableMapping[str, Any] = {}
150+
else:
151+
scope = asgi_app.transport.scope
152+
170153
send, recv = _make_send_recv_callbacks(socket)
171-
conn = Connection(request, socket, path)
172154
await serve_json_patch(
173-
Layout(ConnectionContext(constructor(), value=conn)),
155+
Layout(
156+
ConnectionContext(
157+
constructor(),
158+
value=Connection(
159+
scope=scope,
160+
location=Location(
161+
pathname=f"/{path}",
162+
search=(
163+
f"?{request.query_string}"
164+
if request.query_string
165+
else ""
166+
),
167+
),
168+
carrier=_SanicCarrier(request, socket),
169+
),
170+
)
171+
),
174172
send,
175173
recv,
176174
)
@@ -180,7 +178,7 @@ async def model_stream(
180178

181179

182180
def _make_send_recv_callbacks(
183-
socket: WebSocketCommonProtocol,
181+
socket: WebSocketConnection,
184182
) -> Tuple[SendCoroutine, RecvCoroutine]:
185183
async def sock_send(value: VdomJsonPatch) -> None:
186184
await socket.send(json.dumps(value))
@@ -192,3 +190,14 @@ async def sock_recv() -> LayoutEvent:
192190
return LayoutEvent(**json.loads(data))
193191

194192
return sock_send, sock_recv
193+
194+
195+
@dataclass
196+
class _SanicCarrier:
197+
"""A simple wrapper for holding connection information"""
198+
199+
request: request.Request
200+
"""The current request object"""
201+
202+
websocket: WebSocketConnection
203+
"""A handle to the current websocket"""

0 commit comments

Comments
 (0)