Skip to content

Commit 29bac3a

Browse files
committed
Fixes #157 add per-client view parameters via HTML query
Along with these per-client view parameters we add a `multiview` widget that allows many elements to be dynamically and imperatively added to an already mounted view
1 parent 5c38c63 commit 29bac3a

File tree

10 files changed

+189
-79
lines changed

10 files changed

+189
-79
lines changed

examples/example_utils.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
11
import os
22
from IPython import display
3-
from typing import Mapping, Any, Optional, Callable, Tuple, Type
3+
from typing import Mapping, Any, Optional
44

5-
from idom.server.base import AbstractRenderServer
6-
from idom.server import imperative_server_mount
75

8-
9-
def setup_example_server(
10-
server: Type[AbstractRenderServer], host: str, port: int, shared: bool = False,
11-
) -> Tuple[str, AbstractRenderServer, Callable[..., Any]]:
6+
def example_server_url(host: str, port: int) -> str:
127
localhost_idom_path = f"http://{host}:{port}"
138
jupyterhub_idom_path = path_to_jupyterhub_proxy(port)
14-
path_to_idom = jupyterhub_idom_path or localhost_idom_path
15-
16-
server_instance, mount = imperative_server_mount(
17-
server, host, port, shared, {"cors": True}, {"access_log": False}
18-
)
19-
20-
return path_to_idom, server_instance, mount
9+
return jupyterhub_idom_path or localhost_idom_path
2110

2211

2312
def path_to_jupyterhub_proxy(port: int) -> Optional[str]:

examples/introduction.ipynb

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,18 @@
6969
"metadata": {},
7070
"outputs": [],
7171
"source": [
72+
"from idom.server import multiview_server\n",
7273
"from idom.server.sanic import PerClientState\n",
73-
"from example_utils import setup_example_server, display_href, pretty_dict_string\n",
74+
"from example_utils import display_href, example_server_url, pretty_dict_string\n",
7475
"\n",
75-
"server_url, server, mount = setup_example_server(PerClientState, \"127.0.0.1\", 8765)\n",
76+
"mount, server = multiview_server(\n",
77+
" PerClientState, \"127.0.0.1\", 8765, {\"cors\": True}, {\"access_log\": False}\n",
78+
")\n",
79+
"server_url = example_server_url(\"127.0.0.1\", 8765)\n",
7680
"\n",
7781
"def display(element, *args, **kwargs):\n",
78-
" mount(element, *args, **kwargs)\n",
79-
" return idom.display(\"jupyter\", server_url)"
82+
" view_id = mount(element, *args, **kwargs)\n",
83+
" return idom.display(\"jupyter\", server_url, {\"view_id\": view_id})"
8084
]
8185
},
8286
{
@@ -121,7 +125,8 @@
121125
"source": [
122126
"print(\"Try clicking the image! 🖱️\")\n",
123127
"\n",
124-
"display(Slideshow)"
128+
"slideshow_view = display(Slideshow)\n",
129+
"slideshow_view"
125130
]
126131
},
127132
{
@@ -139,7 +144,7 @@
139144
"metadata": {},
140145
"outputs": [],
141146
"source": [
142-
"display_href(server_url + \"/client/index.html\")"
147+
"display_href(server_url + f\"/client/index.html?{slideshow_view.query}\")"
143148
]
144149
},
145150
{
@@ -699,7 +704,7 @@
699704
"\n",
700705
"display(\n",
701706
" chart.ClickableChart,\n",
702-
" {\"onClick\": handle_event, \"style\": {\"parent\": {\"width\": \"500px\"}}},\n",
707+
" {\"onClick\": handle_event, \"style\": {\"parent\": {\"width\": \"500px\"}}}\n",
703708
")"
704709
]
705710
},
@@ -757,11 +762,13 @@
757762
"metadata": {},
758763
"outputs": [],
759764
"source": [
765+
"from idom.server import hotswap_server\n",
760766
"from idom.server.sanic import SharedClientState\n",
761-
"from example_utils import setup_example_server, display_href, pretty_dict_string\n",
767+
"from example_utils import display_href, example_server_url, pretty_dict_string\n",
762768
"\n",
763-
"shared_server_url, shared_server, mount_shared = setup_example_server(\n",
764-
" SharedClientState, \"127.0.0.1\", 5678, shared=True\n",
769+
"shared_server_url = example_server_url(\"127.0.0.1\", 5678)\n",
770+
"mount_shared, shared_server = hotswap_server(\n",
771+
" SharedClientState, \"127.0.0.1\", 5678, {\"cors\": True}, {\"access_log\": False}\n",
765772
")\n",
766773
"\n",
767774
"def display_shared(element, *args, **kwargs):\n",
@@ -773,7 +780,7 @@
773780
"cell_type": "markdown",
774781
"metadata": {},
775782
"source": [
776-
"## We Can Just Reuse the <a href=\"#Drag-and-Drop---Complex-Interface-Features\">Drap and Drop Example</a>"
783+
"## We Can Just Reuse the <a href=\"#Slideshows---Basic-Interactivity\">Slideshow Example</a>"
777784
]
778785
},
779786
{
@@ -782,7 +789,7 @@
782789
"metadata": {},
783790
"outputs": [],
784791
"source": [
785-
"view = display_shared(DragDropBoxes)"
792+
"view = display_shared(Slideshow)"
786793
]
787794
},
788795
{

idom/client/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
} else {
2222
protocol = "ws:";
2323
}
24-
let endpoint = protocol + "//" + url.join("/");
24+
25+
let endpoint = protocol + "//" + url.join("/") + window.location.search;
2526

2627
renderLayout(document.getElementById("app"), endpoint);
2728
</script>

idom/server/__init__.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar
33

44
from idom.core.element import ElementConstructor
5-
from idom.widgets.common import hotswap
5+
from idom.widgets.common import multiview, hotswap
66

77
from .base import AbstractRenderServer
88

@@ -23,34 +23,65 @@
2323
_S = TypeVar("_S", bound=AbstractRenderServer[Any, Any])
2424

2525

26-
def imperative_server_mount(
26+
def multiview_server(
2727
server: Type[_S],
2828
host: str,
2929
port: int,
30-
shared: bool = False,
3130
server_options: Optional[Any] = None,
3231
run_options: Optional[Dict[str, Any]] = None,
33-
) -> Tuple[_S, Callable[[ElementConstructor], None]]:
34-
"""Set up a server whose view can be swapped out on the fly.
32+
) -> Tuple[Callable[[ElementConstructor], int], _S]:
33+
"""Set up a server where views can be dynamically added.
34+
35+
In other words this allows the user to work with IDOM in an imperative manner.
36+
Under the hood this uses the :func:`idom.widgets.common.multiview` function to
37+
add the views on the fly.
38+
39+
Parameters:
40+
server: The server type to start up as a daemon
41+
host: The server hostname
42+
port: The server port number
43+
server_options: Value passed to :meth:`AbstractRenderServer.configure`
44+
run_options: Keyword args passed to :meth:`AbstractRenderServer.daemon`
45+
46+
Returns:
47+
The server instance and a function for adding views.
48+
See :func:`idom.widgets.common.multiview` for details.
49+
"""
50+
mount, element = multiview()
51+
server_instance = server(element)
52+
if server_options:
53+
server_instance.configure(server_options)
54+
server_instance.daemon(host, port, **(run_options or {}))
55+
return mount, server_instance
56+
57+
58+
def hotswap_server(
59+
server: Type[_S],
60+
host: str,
61+
port: int,
62+
server_options: Optional[Any] = None,
63+
run_options: Optional[Dict[str, Any]] = None,
64+
) -> Tuple[Callable[[ElementConstructor], None], _S]:
65+
"""Set up a server where views can be dynamically swapped out.
3566
3667
In other words this allows the user to work with IDOM in an imperative manner.
3768
Under the hood this uses the :func:`idom.widgets.common.hotswap` function to
38-
switch out views on the fly.
69+
swap the views on the fly.
3970
4071
Parameters:
4172
server: The server type to start up as a daemon
4273
host: The server hostname
4374
port: The server port number
44-
shared: Whether or not all views from the server should be updated when swapping
4575
server_options: Value passed to :meth:`AbstractRenderServer.configure`
4676
run_options: Keyword args passed to :meth:`AbstractRenderServer.daemon`
4777
4878
Returns:
49-
The server instance and a function for swapping out the view.
79+
The server instance and a function for swapping views.
80+
See :func:`idom.widgets.common.hotswap` for details.
5081
"""
51-
mount, element = hotswap(shared)
82+
mount, element = hotswap(shared=True)
5283
server_instance = server(element)
5384
if server_options:
5485
server_instance.configure(server_options)
5586
server_instance.daemon(host, port, **(run_options or {}))
56-
return server_instance, mount
87+
return mount, server_instance

idom/server/base.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from idom.core.element import ElementConstructor, AbstractElement
77
from idom.core.layout import AbstractLayout, Layout
8-
from idom.core.render import AbstractRenderer
8+
from idom.core.render import (
9+
AbstractRenderer,
10+
SendCoroutine,
11+
RecvCoroutine,
12+
)
913

1014

1115
_App = TypeVar("_App", bound=Any)
@@ -89,7 +93,17 @@ def _setup_application(self, app: _App, config: _Config) -> None:
8993
def _run_application(
9094
self, app: _App, config: _Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
9195
) -> Any:
92-
...
96+
raise NotImplementedError()
97+
98+
@abc.abstractmethod
99+
async def _run_renderer(
100+
self,
101+
send: SendCoroutine,
102+
recv: RecvCoroutine,
103+
parameters: Dict[str, Any],
104+
loop: Optional[AbstractEventLoop] = None,
105+
) -> None:
106+
raise NotImplementedError()
93107

94108
def _update_config(self, old: _Config, new: _Config) -> _Config:
95109
"""Return the new configuration options
@@ -101,14 +115,16 @@ def _update_config(self, old: _Config, new: _Config) -> _Config:
101115
return new
102116

103117
def _make_renderer(
104-
self, loop: Optional[AbstractEventLoop] = None
118+
self, parameters: Dict[str, Any], loop: Optional[AbstractEventLoop] = None,
105119
) -> AbstractRenderer:
106-
return self._renderer_type(self._make_layout(loop))
120+
return self._renderer_type(self._make_layout(parameters, loop))
107121

108-
def _make_layout(self, loop: Optional[AbstractEventLoop] = None) -> AbstractLayout:
109-
return self._layout_type(self._make_root_element(), loop)
122+
def _make_layout(
123+
self, parameters: Dict[str, Any], loop: Optional[AbstractEventLoop] = None,
124+
) -> AbstractLayout:
125+
return self._layout_type(self._make_root_element(parameters), loop)
110126

111-
def _make_root_element(self) -> AbstractElement:
127+
def _make_root_element(self, parameters: Dict[str, Any]) -> AbstractElement:
112128
return self._root_element_constructor(
113-
*self._root_element_args, **self._root_element_kwargs
129+
*self._root_element_args, **{**self._root_element_kwargs, **parameters}
114130
)

idom/server/sanic.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import uuid
44

5-
from typing import Tuple, Any, Dict, Union
5+
from typing import Tuple, Any, Dict, Union, Optional
66

77
from sanic import Sanic, request, response
88
from sanic_cors import CORS
@@ -30,9 +30,6 @@ class Config(TypedDict, total=False):
3030
class SanicRenderServer(AbstractRenderServer[Sanic, Config]):
3131
"""Base ``sanic`` extension."""
3232

33-
async def _run_renderer(self, send: SendCoroutine, recv: RecvCoroutine) -> None:
34-
raise NotImplementedError()
35-
3633
def _init_config(self) -> Config:
3734
return Config(cors=False, url_prefix="", webpage_route=True)
3835

@@ -105,7 +102,8 @@ async def sock_send(data: Dict[str, Any]) -> None:
105102
message = {"header": {}, "body": {"render": data}}
106103
await socket.send(json.dumps(message, separators=(",", ":")))
107104

108-
await self._run_renderer(sock_send, sock_recv)
105+
param_dict = {k: request.args.get(k) for k in request.args}
106+
await self._run_renderer(sock_send, sock_recv, param_dict)
109107

110108
async def _client_route(
111109
self, request: request.Request, path: str
@@ -121,8 +119,14 @@ class PerClientState(SanicRenderServer):
121119

122120
_renderer_type = SingleStateRenderer
123121

124-
async def _run_renderer(self, send: SendCoroutine, recv: RecvCoroutine) -> None:
125-
await self._make_renderer().run(send, recv, None)
122+
async def _run_renderer(
123+
self,
124+
send: SendCoroutine,
125+
recv: RecvCoroutine,
126+
parameters: Dict[str, Any],
127+
loop: Optional[asyncio.AbstractEventLoop] = None,
128+
) -> None:
129+
await self._make_renderer(parameters, loop).run(send, recv, None)
126130

127131

128132
class SharedClientState(SanicRenderServer):
@@ -137,7 +141,16 @@ def _setup_application(self, app: Sanic, config: Config) -> None:
137141
async def _setup_renderer(
138142
self, app: Sanic, loop: asyncio.AbstractEventLoop
139143
) -> None:
140-
self._renderer = self._make_renderer(loop)
141-
142-
async def _run_renderer(self, send: SendCoroutine, recv: RecvCoroutine) -> None:
144+
self._renderer = self._make_renderer({}, loop)
145+
146+
async def _run_renderer(
147+
self,
148+
send: SendCoroutine,
149+
recv: RecvCoroutine,
150+
parameters: Dict[str, Any],
151+
loop: Optional[asyncio.AbstractEventLoop] = None,
152+
) -> None:
153+
if parameters:
154+
msg = f"SharedClientState server does not support per-client view parameters {parameters}"
155+
raise ValueError(msg)
143156
await self._renderer.run(send, recv, uuid.uuid4().hex)

idom/widgets/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .common import Import, define_module, hotswap
1+
from .common import Import, define_module, hotswap, multiview
22
from .display import display
33
from .inputs import Input
44
from .images import Image
@@ -10,6 +10,7 @@
1010
"Image",
1111
"define_module",
1212
"hotswap",
13+
"multiview",
1314
"html",
1415
"Input",
1516
"Import",

0 commit comments

Comments
 (0)