Skip to content

Commit f963809

Browse files
committed
convert tornado + begin reworking test utils
1 parent 5a4e666 commit f963809

File tree

12 files changed

+122
-105
lines changed

12 files changed

+122
-105
lines changed

src/idom/core/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
ComponentConstructor = Callable[..., "ComponentType"]
2222
"""Simple function returning a new component"""
2323

24+
RootComponentConstructor = Callable[[], "ComponentType"]
25+
"""The root component should be constructed by a function accepting no arguments."""
26+
2427

2528
Key = Union[str, int]
2629

src/idom/server/any.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import sys
45
import warnings
56
import webbrowser
67
from importlib import import_module
78
from typing import Any, Awaitable, Iterator
89

9-
from idom.types import ComponentConstructor
10+
from idom.types import RootComponentConstructor
1011

1112
from .types import ServerImplementation
1213
from .utils import find_available_port
@@ -22,10 +23,11 @@
2223

2324

2425
def run(
25-
component: ComponentConstructor,
26+
component: RootComponentConstructor,
2627
host: str = "127.0.0.1",
2728
port: int | None = None,
2829
open_browser: bool = True,
30+
implementation: ServerImplementation = sys.modules[__name__],
2931
) -> None:
3032
"""Run a component with a development server"""
3133

@@ -35,16 +37,16 @@ def run(
3537
stacklevel=2,
3638
)
3739

38-
app = create_development_app()
39-
configure(app, component)
40+
app = implementation.create_development_app()
41+
implementation.configure(app, component)
4042

4143
coros: list[Awaitable] = []
4244

4345
host = host
4446
port = port or find_available_port(host)
4547
started = asyncio.Event()
4648

47-
coros.append(serve_development_app(app, host, port, started))
49+
coros.append(implementation.serve_development_app(app, host, port, started))
4850

4951
if open_browser:
5052

@@ -57,25 +59,29 @@ async def _open_browser_after_server() -> None:
5759
asyncio.get_event_loop().run_forever(asyncio.gather(*coros))
5860

5961

60-
def configure(app: Any, component: ComponentConstructor) -> None:
62+
def configure(app: Any, component: RootComponentConstructor) -> None:
6163
"""Configure the given app instance to display the given component"""
62-
return get_implementation().configure(app, component)
64+
return _get_any_implementation().configure(app, component)
6365

6466

6567
def create_development_app() -> Any:
6668
"""Create an application instance for development purposes"""
67-
return get_implementation().create_development_app()
69+
return _get_any_implementation().create_development_app()
6870

6971

7072
async def serve_development_app(
7173
app: Any, host: str, port: int, started: asyncio.Event
7274
) -> None:
7375
"""Run an application using a development server"""
74-
return await get_implementation().serve_development_app(app, host, port, started)
76+
return await _get_any_implementation().serve_development_app(
77+
app, host, port, started
78+
)
7579

7680

77-
def get_implementation() -> ServerImplementation:
81+
def _get_any_implementation() -> ServerImplementation:
7882
"""Get the first available server implementation"""
83+
global _DEFAULT_IMPLEMENTATION
84+
7985
if _DEFAULT_IMPLEMENTATION is not None:
8086
return _DEFAULT_IMPLEMENTATION
8187

@@ -84,7 +90,6 @@ def get_implementation() -> ServerImplementation:
8490
except StopIteration:
8591
raise RuntimeError("No built-in server implementation installed.")
8692
else:
87-
global _DEFAULT_IMPLEMENTATION
8893
_DEFAULT_IMPLEMENTATION = implementation
8994
return implementation
9095

src/idom/server/fastapi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from fastapi import FastAPI
44

55
from idom.config import IDOM_DEBUG_MODE
6-
from idom.core.types import ComponentConstructor
6+
from idom.core.types import RootComponentConstructor
77

88
from .starlette import (
99
Options,
@@ -19,7 +19,7 @@
1919

2020
def configure(
2121
app: FastAPI,
22-
constructor: ComponentConstructor,
22+
constructor: RootComponentConstructor,
2323
options: Options | None = None,
2424
) -> None:
2525
"""Prepare a :class:`FastAPI` server to serve the given component

src/idom/server/flask.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from idom.config import IDOM_DEBUG_MODE, IDOM_WEB_MODULES_DIR
2222
from idom.core.dispatcher import dispatch_single_view
2323
from idom.core.layout import LayoutEvent, LayoutUpdate
24-
from idom.core.types import ComponentConstructor, ComponentType
24+
from idom.core.types import ComponentType, RootComponentConstructor
2525

2626
from .utils import CLIENT_BUILD_DIR
2727

@@ -30,7 +30,7 @@
3030

3131

3232
def configure(
33-
app: Flask, component: ComponentConstructor, options: Options | None = None
33+
app: Flask, component: RootComponentConstructor, options: Options | None = None
3434
) -> None:
3535
"""Return a :class:`FlaskServer` where each client has its own state.
3636
@@ -152,7 +152,7 @@ def redirect_to_index() -> Any:
152152

153153

154154
def _setup_single_view_dispatcher_route(
155-
app: Flask, options: Options, constructor: ComponentConstructor
155+
app: Flask, options: Options, constructor: RootComponentConstructor
156156
) -> None:
157157
sockets = Sockets(app)
158158

src/idom/server/sanic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
dispatch_single_view,
1919
)
2020
from idom.core.layout import Layout, LayoutEvent
21-
from idom.core.types import ComponentConstructor
21+
from idom.core.types import RootComponentConstructor
2222

2323
from .utils import CLIENT_BUILD_DIR
2424

@@ -27,7 +27,7 @@
2727

2828

2929
def configure(
30-
app: Sanic, component: ComponentConstructor, options: Options | None = None
30+
app: Sanic, component: RootComponentConstructor, options: Options | None = None
3131
) -> None:
3232
"""Configure an application instance to display the given component"""
3333
options = _setup_options(options)
@@ -107,7 +107,7 @@ def redirect_to_index(
107107

108108

109109
def _setup_single_view_dispatcher_route(
110-
blueprint: Blueprint, constructor: ComponentConstructor
110+
blueprint: Blueprint, constructor: RootComponentConstructor
111111
) -> None:
112112
@blueprint.websocket("/stream") # type: ignore
113113
async def model_stream(

src/idom/server/starlette.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
dispatch_single_view,
2525
)
2626
from idom.core.layout import Layout, LayoutEvent
27-
from idom.core.types import ComponentConstructor
27+
from idom.core.types import RootComponentConstructor
2828

2929
from .utils import CLIENT_BUILD_DIR
3030

@@ -34,7 +34,7 @@
3434

3535
def configure(
3636
app: Starlette,
37-
constructor: ComponentConstructor,
37+
constructor: RootComponentConstructor,
3838
options: Options | None = None,
3939
) -> None:
4040
"""Return a :class:`StarletteServer` where each client has its own state.
@@ -46,7 +46,7 @@ def configure(
4646
constructor: A component constructor
4747
options: Options for configuring server behavior
4848
"""
49-
options, app = _setup_options(options)
49+
options = _setup_options(options)
5050
_setup_common_routes(options, app)
5151
_setup_single_view_dispatcher_route(options["url_prefix"], app, constructor)
5252

@@ -154,7 +154,7 @@ def redirect_to_index(request: Request) -> RedirectResponse:
154154

155155

156156
def _setup_single_view_dispatcher_route(
157-
url_prefix: str, app: Starlette, constructor: ComponentConstructor
157+
url_prefix: str, app: Starlette, constructor: RootComponentConstructor
158158
) -> None:
159159
@app.websocket_route(f"{url_prefix}/stream")
160160
async def model_stream(socket: WebSocket) -> None:

src/idom/server/tornado.py

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
from asyncio import Queue as AsyncQueue
66
from asyncio.futures import Future
7+
from concurrent.futures import ThreadPoolExecutor
78
from threading import Event as ThreadEvent
89
from typing import Any, List, Optional, Tuple, Type, Union
910
from urllib.parse import urljoin
@@ -21,11 +22,45 @@
2122
from .utils import CLIENT_BUILD_DIR, threaded, wait_on_event
2223

2324

24-
_RouteHandlerSpecs = List[Tuple[str, Type[RequestHandler], Any]]
25+
def configure(
26+
app: Application,
27+
component: ComponentConstructor,
28+
options: Options | None = None,
29+
) -> TornadoServer:
30+
"""Return a :class:`TornadoServer` where each client has its own state.
31+
32+
Implements the :class:`~idom.server.proto.ServerFactory` protocol
33+
34+
Parameters:
35+
app: A tornado ``Application`` instance.
36+
component: A root component constructor
37+
options: Options for configuring how the component is mounted to the server.
38+
"""
39+
options = _setup_options(options)
40+
_add_handler(
41+
app,
42+
options,
43+
_setup_common_routes(options) + _setup_single_view_dispatcher_route(component),
44+
)
45+
return TornadoServer(app)
2546

2647

27-
class Config(TypedDict, total=False):
28-
"""Render server config for :class:`TornadoRenderServer` subclasses"""
48+
def create_development_app() -> Application:
49+
return Application(debug=True)
50+
51+
52+
async def serve_development_app(
53+
app: Application, host: str, port: int, started: asyncio.Event
54+
) -> None:
55+
loop = AsyncIOMainLoop()
56+
loop.install()
57+
app.listen(port, host)
58+
loop.add_callback(lambda: loop.asyncio_loop.call_soon_threadsafe(started.set))
59+
await loop.run_in_executor(ThreadPoolExecutor())
60+
61+
62+
class Options(TypedDict, total=False):
63+
"""Render server options for :class:`TornadoRenderServer` subclasses"""
2964

3065
redirect_root_to_index: bool
3166
"""Whether to redirect the root URL (with prefix) to ``index.html``"""
@@ -37,27 +72,7 @@ class Config(TypedDict, total=False):
3772
"""The URL prefix where IDOM resources will be served from"""
3873

3974

40-
def PerClientStateServer(
41-
constructor: ComponentConstructor,
42-
config: Optional[Config] = None,
43-
app: Optional[Application] = None,
44-
) -> TornadoServer:
45-
"""Return a :class:`TornadoServer` where each client has its own state.
46-
47-
Implements the :class:`~idom.server.proto.ServerFactory` protocol
48-
49-
Parameters:
50-
constructor: A component constructor
51-
config: Options for configuring server behavior
52-
app: An application instance (otherwise a default instance is created)
53-
"""
54-
config, app = _setup_config_and_app(config, app)
55-
_add_handler(
56-
app,
57-
config,
58-
_setup_common_routes(config) + _setup_single_view_dispatcher_route(constructor),
59-
)
60-
return TornadoServer(app)
75+
_RouteHandlerSpecs = List[Tuple[str, Type[RequestHandler], Any]]
6176

6277

6378
class TornadoServer:
@@ -105,23 +120,18 @@ def stop() -> None:
105120
wait_on_event(f"stop {self.app}", did_stop, timeout)
106121

107122

108-
def _setup_config_and_app(
109-
config: Optional[Config], app: Optional[Application]
110-
) -> Tuple[Config, Application]:
111-
return (
112-
{
113-
"url_prefix": "",
114-
"serve_static_files": True,
115-
"redirect_root_to_index": True,
116-
**(config or {}), # type: ignore
117-
},
118-
app or Application(),
119-
)
123+
def _setup_options(options: Options | None) -> Options:
124+
return {
125+
"url_prefix": "",
126+
"serve_static_files": True,
127+
"redirect_root_to_index": True,
128+
**(options or {}), # type: ignore
129+
}
120130

121131

122-
def _setup_common_routes(config: Config) -> _RouteHandlerSpecs:
132+
def _setup_common_routes(options: Options) -> _RouteHandlerSpecs:
123133
handlers: _RouteHandlerSpecs = []
124-
if config["serve_static_files"]:
134+
if options["serve_static_files"]:
125135
handlers.append(
126136
(
127137
r"/client/(.*)",
@@ -136,16 +146,16 @@ def _setup_common_routes(config: Config) -> _RouteHandlerSpecs:
136146
{"path": str(IDOM_WEB_MODULES_DIR.current)},
137147
)
138148
)
139-
if config["redirect_root_to_index"]:
149+
if options["redirect_root_to_index"]:
140150
handlers.append(("/", RedirectHandler, {"url": "./client/index.html"}))
141151
return handlers
142152

143153

144154
def _add_handler(
145-
app: Application, config: Config, handlers: _RouteHandlerSpecs
155+
app: Application, options: Options, handlers: _RouteHandlerSpecs
146156
) -> None:
147157
prefixed_handlers: List[Any] = [
148-
(urljoin(config["url_prefix"], route_pattern),) + tuple(handler_info)
158+
(urljoin(options["url_prefix"], route_pattern),) + tuple(handler_info)
149159
for route_pattern, *handler_info in handlers
150160
]
151161
app.add_handlers(r".*", prefixed_handlers)
@@ -157,13 +167,13 @@ def _setup_single_view_dispatcher_route(
157167
return [
158168
(
159169
"/stream",
160-
PerClientStateModelStreamHandler,
170+
ModelStreamHandler,
161171
{"component_constructor": constructor},
162172
)
163173
]
164174

165175

166-
class PerClientStateModelStreamHandler(WebSocketHandler):
176+
class ModelStreamHandler(WebSocketHandler):
167177
"""A web-socket handler that serves up a new model stream to each new client"""
168178

169179
_dispatch_future: Future[None]

src/idom/server/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from typing_extensions import Protocol, runtime_checkable
55

6-
from idom.core.types import ComponentType
6+
from idom.core.types import RootComponentConstructor
77

88

99
_App = TypeVar("_App")
@@ -13,7 +13,7 @@
1313
class ServerImplementation(Protocol):
1414
"""Common interface for IDOM's builti-in server implementations"""
1515

16-
def configure(self, app: _App, component: Callable[[], ComponentType]) -> None:
16+
def configure(self, app: _App, component: RootComponentConstructor) -> None:
1717
"""Configure the given app instance to display the given component"""
1818

1919
def create_development_app(self) -> _App:

0 commit comments

Comments
 (0)