Skip to content

Commit abed2d3

Browse files
committed
rework as location object
1 parent 2849f63 commit abed2d3

File tree

11 files changed

+176
-71
lines changed

11 files changed

+176
-71
lines changed

src/idom/server/default.py

+6
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ async def serve_development_app(
3636

3737

3838
def use_scope() -> Any:
39+
"""Return the current ASGI/WSGI scope"""
3940
return _default_implementation().use_scope()
4041

4142

43+
def use_route() -> str:
44+
"""Return the current route as a string"""
45+
return _default_implementation().use_route()
46+
47+
4248
_DEFAULT_IMPLEMENTATION: ServerImplementation[Any] | None = None
4349

4450

src/idom/server/fastapi.py

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
serve_development_app = starlette.serve_development_app
99
"""Alias for :func:`idom.server.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
13+
"""Alias for :func:`idom.server.starlette.use_location`"""
14+
1115
# see: https://github.com/idom-team/flake8-idom-hooks/issues/12
1216
use_scope = starlette.use_scope # noqa: ROH101
1317
"""Alias for :func:`idom.server.starlette.use_scope`"""

src/idom/server/flask.py

+30-22
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from idom.core.layout import LayoutEvent, LayoutUpdate
3030
from idom.core.serve import serve_json_patch
3131
from idom.core.types import ComponentType, RootComponentConstructor
32+
from idom.server.types import Location
3233
from idom.utils import Ref
3334

3435
from .utils import safe_client_build_dir_path, safe_web_modules_dir_path
@@ -110,6 +111,23 @@ def run_server() -> None:
110111
raise RuntimeError("Failed to shutdown server.")
111112

112113

114+
def use_location() -> Location:
115+
"""Get the current route as a string"""
116+
conn = use_connection()
117+
search = conn.request.query_string.decode()
118+
return Location(pathname="/" + conn.path, search="?" + search if search else "")
119+
120+
121+
def use_scope() -> dict[str, Any]:
122+
"""Get the current WSGI environment"""
123+
return use_request().environ
124+
125+
126+
def use_request() -> Request:
127+
"""Get the current ``Request``"""
128+
return use_connection().request
129+
130+
113131
def use_connection() -> Connection:
114132
"""Get the current :class:`Connection`"""
115133
connection = use_context(ConnectionContext)
@@ -120,14 +138,18 @@ def use_connection() -> Connection:
120138
return connection
121139

122140

123-
def use_request() -> Request:
124-
"""Get the current ``Request``"""
125-
return use_connection().request
141+
@dataclass
142+
class Connection:
143+
"""A simple wrapper for holding connection information"""
126144

145+
request: Request
146+
"""The current request object"""
127147

128-
def use_scope() -> dict[str, Any]:
129-
"""Get the current WSGI environment"""
130-
return use_request().environ
148+
websocket: WebSocket
149+
"""A handle to the current websocket"""
150+
151+
path: str
152+
"""The current path being served"""
131153

132154

133155
@dataclass
@@ -181,13 +203,13 @@ def send(value: Any) -> None:
181203
def recv() -> LayoutEvent:
182204
return LayoutEvent(**json.loads(ws.receive()))
183205

184-
dispatch_in_thread(ws, path, constructor(), send, recv)
206+
_dispatch_in_thread(ws, path, constructor(), send, recv)
185207

186208
sock.route("/_api/stream", endpoint="without_path")(model_stream)
187209
sock.route("/<path:path>/_api/stream", endpoint="with_path")(model_stream)
188210

189211

190-
def dispatch_in_thread(
212+
def _dispatch_in_thread(
191213
websocket: WebSocket,
192214
path: str,
193215
component: ComponentType,
@@ -260,20 +282,6 @@ def run_send() -> None:
260282
)
261283

262284

263-
@dataclass
264-
class Connection:
265-
"""A simple wrapper for holding connection information"""
266-
267-
request: Request
268-
"""The current request object"""
269-
270-
websocket: WebSocket
271-
"""A handle to the current websocket"""
272-
273-
path: str
274-
"""The current path being served"""
275-
276-
277285
class _DispatcherThreadInfo(NamedTuple):
278286
dispatch_loop: asyncio.AbstractEventLoop
279287
dispatch_future: "asyncio.Future[Any]"

src/idom/server/sanic.py

+33-25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
serve_json_patch,
2525
)
2626
from idom.core.types import RootComponentConstructor
27+
from idom.server.types import Location
2728

2829
from ._asgi import serve_development_asgi
2930
from .utils import safe_client_build_dir_path, safe_web_modules_dir_path
@@ -66,6 +67,28 @@ async def serve_development_app(
6667
await serve_development_asgi(app, host, port, started)
6768

6869

70+
def use_location() -> Location:
71+
"""Get the current route as a string"""
72+
conn = use_connection()
73+
search = conn.request.query_string
74+
return Location(pathname="/" + conn.path, search="?" + search if search else "")
75+
76+
77+
def use_scope() -> ASGIScope:
78+
"""Get the current ASGI scope"""
79+
app = use_request().app
80+
try:
81+
asgi_app = app._asgi_app
82+
except AttributeError: # pragma: no cover
83+
raise RuntimeError("No scope. Sanic may not be running with an ASGI server")
84+
return asgi_app.transport.scope
85+
86+
87+
def use_request() -> request.Request:
88+
"""Get the current ``Request``"""
89+
return use_connection().request
90+
91+
6992
def use_connection() -> Connection:
7093
"""Get the current :class:`Connection`"""
7194
connection = use_context(ConnectionContext)
@@ -76,19 +99,18 @@ def use_connection() -> Connection:
7699
return connection
77100

78101

79-
def use_request() -> request.Request:
80-
"""Get the current ``Request``"""
81-
return use_connection().request
102+
@dataclass
103+
class Connection:
104+
"""A simple wrapper for holding connection information"""
82105

106+
request: request.Request
107+
"""The current request object"""
83108

84-
def use_scope() -> ASGIScope:
85-
"""Get the current ASGI scope"""
86-
app = use_request().app
87-
try:
88-
asgi_app = app._asgi_app
89-
except AttributeError: # pragma: no cover
90-
raise RuntimeError("No scope. Sanic may not be running with an ASGI server")
91-
return asgi_app.transport.scope
109+
websocket: WebSocketCommonProtocol
110+
"""A handle to the current websocket"""
111+
112+
path: str
113+
"""The current path being served"""
92114

93115

94116
@dataclass
@@ -156,20 +178,6 @@ async def model_stream(
156178
blueprint.add_websocket_route(model_stream, "/<path:path>/_api/stream")
157179

158180

159-
@dataclass
160-
class Connection:
161-
"""A simple wrapper for holding connection information"""
162-
163-
request: request.Request
164-
"""The current request object"""
165-
166-
websocket: WebSocketCommonProtocol
167-
"""A handle to the current websocket"""
168-
169-
path: str
170-
"""The current path being served"""
171-
172-
173181
def _make_send_recv_callbacks(
174182
socket: WebSocketCommonProtocol,
175183
) -> Tuple[SendCoroutine, RecvCoroutine]:

src/idom/server/starlette.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
serve_json_patch,
2323
)
2424
from idom.core.types import RootComponentConstructor
25+
from idom.server.types import Location
2526

2627
from ._asgi import serve_development_asgi
2728
from .utils import CLIENT_BUILD_DIR, safe_client_build_dir_path
@@ -71,6 +72,19 @@ async def serve_development_app(
7172
await serve_development_asgi(app, host, port, started)
7273

7374

75+
def use_location() -> Location:
76+
"""Get the current route as a string"""
77+
scope = use_scope()
78+
pathname = "/" + scope["path_params"].get("path", "")
79+
search = scope["query_string"].decode()
80+
return Location(pathname, "?" + search if search else "")
81+
82+
83+
def use_scope() -> Scope:
84+
"""Get the current ASGI scope dictionary"""
85+
return use_websocket().scope
86+
87+
7488
def use_websocket() -> WebSocket:
7589
"""Get the current WebSocket object"""
7690
websocket = use_context(WebSocketContext)
@@ -81,11 +95,6 @@ def use_websocket() -> WebSocket:
8195
return websocket
8296

8397

84-
def use_scope() -> Scope:
85-
"""Get the current ASGI scope dictionary"""
86-
return use_websocket().scope
87-
88-
8998
@dataclass
9099
class Options:
91100
"""Optionsuration options for :class:`StarletteRenderServer`"""
@@ -147,9 +156,6 @@ def _setup_single_view_dispatcher_route(
147156
@app.websocket_route(options.url_prefix + "/_api/stream")
148157
@app.websocket_route(options.url_prefix + "/{path:path}/_api/stream")
149158
async def model_stream(socket: WebSocket) -> None:
150-
path_params: dict[str, str] = socket.scope["path_params"]
151-
path_params["path"] = "/" + path_params.get("path", "")
152-
153159
await socket.accept()
154160
send, recv = _make_send_recv_callbacks(socket)
155161
try:

src/idom/server/tornado.py

+37-11
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
from idom.core.layout import Layout, LayoutEvent
2222
from idom.core.serve import VdomJsonPatch, serve_json_patch
2323
from idom.core.types import ComponentConstructor
24+
from idom.server.types import Location
2425

2526
from .utils import CLIENT_BUILD_DIR, safe_client_build_dir_path
2627

2728

28-
RequestContext: type[Context[HTTPServerRequest | None]] = create_context(
29-
None, "RequestContext"
29+
ConnectionContext: type[Context[HTTPServerRequest | None]] = create_context(
30+
None, "ConnectionContext"
3031
)
3132

3233

@@ -88,19 +89,41 @@ async def serve_development_app(
8889
await server.close_all_connections()
8990

9091

92+
def use_location() -> Location:
93+
"""Get the current route as a string"""
94+
conn = use_connection()
95+
search = conn.request.query
96+
return Location(pathname="/" + conn.path, search="?" + search if search else "")
97+
98+
99+
def use_scope() -> dict[str, Any]:
100+
"""Get the current WSGI environment dictionary"""
101+
return WSGIContainer.environ(use_request())
102+
103+
91104
def use_request() -> HTTPServerRequest:
92105
"""Get the current ``HTTPServerRequest``"""
93-
request = use_context(RequestContext)
94-
if request is None:
106+
return use_connection().request
107+
108+
109+
def use_connection() -> Connection:
110+
connection = use_context(ConnectionContext)
111+
if connection is None:
95112
raise RuntimeError( # pragma: no cover
96-
"No request. Are you running with a Tornado server?"
113+
"No connection. Are you running with a Tornado server?"
97114
)
98-
return request
115+
return connection
99116

100117

101-
def use_scope() -> dict[str, Any]:
102-
"""Get the current WSGI environment dictionary"""
103-
return WSGIContainer.environ(use_request())
118+
@dataclass
119+
class Connection:
120+
"""A simple wrapper for holding connection information"""
121+
122+
request: HTTPServerRequest
123+
"""The current request object"""
124+
125+
path: str
126+
"""The current path being served"""
104127

105128

106129
@dataclass
@@ -179,7 +202,7 @@ class ModelStreamHandler(WebSocketHandler):
179202
def initialize(self, component_constructor: ComponentConstructor) -> None:
180203
self._component_constructor = component_constructor
181204

182-
async def open(self, *args: Any, **kwargs: Any) -> None:
205+
async def open(self, path: str) -> None:
183206
message_queue: "AsyncQueue[str]" = AsyncQueue()
184207

185208
async def send(value: VdomJsonPatch) -> None:
@@ -192,7 +215,10 @@ async def recv() -> LayoutEvent:
192215
self._dispatch_future = asyncio.ensure_future(
193216
serve_json_patch(
194217
Layout(
195-
RequestContext(self._component_constructor(), value=self.request)
218+
ConnectionContext(
219+
self._component_constructor(),
220+
value=Connection(self.request, path),
221+
)
196222
),
197223
send,
198224
recv,

src/idom/server/types.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
from dataclasses import dataclass
45
from typing import Any, MutableMapping, TypeVar
56

67
from typing_extensions import Protocol, runtime_checkable
@@ -37,3 +38,21 @@ async def serve_development_app(
3738

3839
def use_scope(self) -> MutableMapping[str, Any]:
3940
"""Get an ASGI scope or WSGI environment dictionary"""
41+
42+
def use_location(self) -> Location:
43+
"""Get the current location (URL)"""
44+
45+
46+
@dataclass
47+
class Location:
48+
"""Represents the current location (URL)
49+
50+
Analogous to, but not necessarily identical to, the client-side
51+
``document.location`` object.
52+
"""
53+
54+
pathname: str
55+
"""the path of the URL for the location"""
56+
57+
search: str = ""
58+
"""A search or query string - a '?' followed by the parameters of the URL."""

src/idom/testing/display.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@ def __init__(
3434
async def show(
3535
self,
3636
component: RootComponentConstructor,
37-
query: dict[str, Any] | None = None,
3837
) -> None:
3938
self._next_view_id += 1
4039
view_id = f"display-{self._next_view_id}"
4140
self.server.mount(lambda: html.div({"id": view_id}, component()))
4241

43-
await self.page.goto(self.server.url(query=query))
42+
await self.goto("/")
4443
await self.page.wait_for_selector(f"#{view_id}", state="attached")
4544

45+
async def goto(self, path: str, query: Any | None = None) -> None:
46+
await self.page.goto(self.server.url(path, query))
47+
4648
async def __aenter__(self) -> DisplayFixture:
4749
es = self._exit_stack = AsyncExitStack()
4850

0 commit comments

Comments
 (0)