Skip to content

Commit ef17e11

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

File tree

12 files changed

+184
-85
lines changed

12 files changed

+184
-85
lines changed

src/idom/server/default.py

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

66
from idom.types import RootComponentConstructor
77

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

1111

@@ -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_location() -> Location:
44+
"""Return the current route as a string"""
45+
return _default_implementation().use_location()
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/server/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def run(
6161

6262
def safe_client_build_dir_path(path: str) -> Path:
6363
"""Prevent path traversal out of :data:`CLIENT_BUILD_DIR`"""
64-
start, _, end = path.rpartition("/")
64+
start, _, end = (path[:-1] if path.endswith("/") else path).rpartition("/")
6565
file = end or start
6666
final_path = traversal_safe_path(CLIENT_BUILD_DIR, file)
6767
return final_path if final_path.is_file() else (CLIENT_BUILD_DIR / "index.html")

0 commit comments

Comments
 (0)