def __init__(self, component: ComponentType): super().__init__(_import_source_base_url=_IMPORT_SOURCE_BASE_URL) self._idom_model = {} self._idom_views = set() self._idom_layout = Layout(component) self._idom_loop = _spawn_threaded_event_loop(self._idom_layout_render_loop()) self.on_msg(lambda _, *args, **kwargs: self._idom_on_msg(*args, **kwargs))
def _setup(self): if self.object is None: return from idom.core.component import Component from idom.core.layout import Layout if isinstance(self.object, Layout): self._idom_layout = self.object elif isinstance(self.object, Component): self._idom_layout = Layout(self.object) else: self._idom_layout = Layout(self.object()) self._idom_loop = _spawn_threaded_event_loop(self._idom_layout_render_loop())
async def test_changing_key_of_component_resets_state(): set_key = Ref() did_init_state = Ref(0) hook = HookCatcher() @component @hook.capture def Root(): key, set_key.current = use_state("key-1") return Child(key=key) @component def Child(): use_state( lambda: did_init_state.set_current(did_init_state.current + 1)) async with Layout(Root()) as layout: await layout.render() assert did_init_state.current == 1 set_key.current("key-2") await layout.render() assert did_init_state.current == 2 hook.latest.schedule_render() await layout.render() assert did_init_state.current == 2
async def test_shared_state_renderer_deletes_old_elements(): sent = [] target_id = "some-id" async def send(data): if len(sent) == 2: raise asyncio.CancelledError() sent.append(data) async def recv(): await asyncio.sleep(0) return LayoutEvent(target_id, []) @idom.element async def Outer(self): @idom.event(target_id=target_id) async def an_event(): self.update() return idom.html.div({"onEvent": an_event}, Inner()) @idom.element async def Inner(self): return idom.html.div() layout = Layout(Outer()) async with SharedStateRenderer(layout) as renderer: await renderer.run(send, recv, "1") root = sent[0]["new"][layout.root] first_inner_id = root["children"][0]["data"] assert sent[1]["old"] == [first_inner_id]
class LayoutWidget(widgets.DOMWidget): """A widget for displaying IDOM elements""" # Name of the widget view class in front-end _view_name = Unicode("IdomView").tag(sync=True) # Name of the widget model class in front-end _model_name = Unicode("IdomModel").tag(sync=True) # Name of the front-end module containing widget view _view_module = Unicode("idom-client-jupyter").tag(sync=True) # Name of the front-end module containing widget model _model_module = Unicode("idom-client-jupyter").tag(sync=True) # Version of the front-end module containing widget view _view_module_version = Unicode("^0.8.0").tag(sync=True) # Version of the front-end module containing widget model _model_module_version = Unicode("^0.8.0").tag(sync=True) _import_source_base_url = Unicode().tag(sync=True) def __init__(self, component: ComponentType): super().__init__(_import_source_base_url=_IMPORT_SOURCE_BASE_URL) self._idom_model = {} self._idom_views = set() self._idom_layout = Layout(component) self._idom_loop = _spawn_threaded_event_loop(self._idom_layout_render_loop()) self.on_msg(lambda _, *args, **kwargs: self._idom_on_msg(*args, **kwargs)) def _idom_on_msg(self, message, buffers): m_type = message.get("type") if m_type == "client-ready": v_id = message["viewID"] self._idom_views.add(v_id) update = LayoutUpdate("", None, self._idom_model) diff = VdomJsonPatch.create_from(update) self.send({"viewID": v_id, "data": diff}) elif m_type == "dom-event": asyncio.run_coroutine_threadsafe( self._idom_layout.deliver(LayoutEvent(**message["data"])), loop=self._idom_loop, ) elif m_type == "client-removed": v_id = message["viewID"] if v_id in self._idom_views: self._idom_views.remove(message["viewID"]) async def _idom_layout_render_loop(self): with self._idom_layout: while True: diff = await render_json_patch(self._idom_layout) self._idom_model = diff.apply_to(self._idom_model) for v_id in self._idom_views: self.send({"viewID": v_id, "data": diff}) def __repr__(self) -> str: return f"LayoutWidget({self._idom_layout})"
async def model_stream(request: request.Request, socket: WebSocketCommonProtocol, path: str = "") -> None: send, recv = _make_send_recv_callbacks(socket) conn = Connection(request, socket, path) await serve_json_patch( Layout(ConnectionContext(constructor(), value=conn)), send, recv, )
async def model_stream(socket: WebSocket) -> None: await socket.accept() send, recv = _make_send_recv_callbacks(socket) try: await serve_json_patch( Layout(WebSocketContext(constructor(), value=socket)), send, recv, ) except WebSocketDisconnect as error: logger.info(f"WebSocket disconnect: {error.code}")
async def test_dispatcher_start_stop(): cancelled_recv = False cancelled_send = False async def send(patch): nonlocal cancelled_send try: await asyncio.sleep(100) except asyncio.CancelledError: cancelled_send = True raise else: assert False, "this should never be reached" async def recv(): nonlocal cancelled_recv try: await asyncio.sleep(100) except asyncio.CancelledError: cancelled_recv = True raise else: assert False, "this should never be reached" @idom.component def AnyComponent(): return idom.html.div() dispatcher = SingleViewDispatcher(Layout(AnyComponent())) await dispatcher.start() await dispatcher.run(send, recv, None) # let it run until it hits the sleeping recv/send calls for i in range(10): await asyncio.sleep(0) await dispatcher.stop() assert cancelled_recv assert cancelled_send
async def test_set_state_during_render(): render_count = Ref(0) @idom.component def SetStateDuringRender(): render_count.current += 1 state, set_state = idom.use_state(0) if not state: set_state(state + 1) return html.div(state) async with Layout(SetStateDuringRender()) as layout: await layout.render() assert render_count.current == 1 await layout.render() assert render_count.current == 2 # there should be no more renders to perform with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(layout.render(), timeout=0.1)
async def open(self, path: str = "", *args: Any, **kwargs: Any) -> None: message_queue: "AsyncQueue[str]" = AsyncQueue() async def send(value: VdomJsonPatch) -> None: await self.write_message(json.dumps(value)) async def recv() -> LayoutEvent: return LayoutEvent(**json.loads(await message_queue.get())) self._message_queue = message_queue self._dispatch_future = asyncio.ensure_future( serve_json_patch( Layout( ConnectionContext( self._component_constructor(), value=Connection(self.request, path), )), send, recv, ))
async def test_shared_state_renderer(): done = asyncio.Event() data_sent_1 = asyncio.Queue() data_sent_2 = [] async def send_1(data): await data_sent_1.put(data) async def recv_1(): sent = await data_sent_1.get() element_id = sent["root"] element_data = sent["new"][element_id] if element_data["attributes"]["count"] == 4: done.set() raise asyncio.CancelledError() return LayoutEvent(target="an-event", data=[]) async def send_2(data): element_id = data["root"] element_data = data["new"][element_id] data_sent_2.append(element_data["attributes"]["count"]) async def recv_2(): await done.wait() raise asyncio.CancelledError() @idom.element async def Clickable(self, count=0): @idom.event(target_id="an-event") async def an_event(): self.update(count=count + 1) return idom.html.div({"anEvent": an_event, "count": count}) async with SharedStateRenderer(Layout(Clickable())) as renderer: await renderer.run(send_1, recv_1, "1") await renderer.run(send_2, recv_2, "2") assert data_sent_2 == [0, 1, 2, 3, 4]
async def open(self, *args: str, **kwargs: str) -> None: message_queue: "AsyncQueue[str]" = AsyncQueue() query_params = { k: v[0].decode() for k, v in self.request.arguments.items() } dispatcher = self._dispatcher_type( Layout(self._component_constructor(**query_params))) async def send(value: LayoutUpdate) -> None: await self.write_message(json.dumps(value)) async def recv() -> LayoutEvent: return LayoutEvent(**json.loads(await message_queue.get())) async def run() -> None: await dispatcher.__aenter__() await dispatcher.run(send, recv, None) asyncio.ensure_future(run()) self._dispatcher_inst = dispatcher self._message_queue = message_queue
async def test_change_element_to_string_causes_unmount(): set_toggle = Ref() did_unmount = Ref(False) @component def Root(): toggle, set_toggle.current = use_toggle(True) if toggle: return html.div(Child()) else: return html.div("some-string") @component def Child(): use_effect(lambda: lambda: did_unmount.set_current(True)) async with Layout(Root()) as layout: await layout.render() set_toggle.current() await layout.render() assert did_unmount.current
async def test_changing_event_handlers_in_the_next_render(): set_event_name = Ref() event_handler = StaticEventHandler() did_trigger = Ref(False) @component def Root(): event_name, set_event_name.current = use_state("first") return html.button({ event_name: event_handler.use(lambda: did_trigger.set_current(True)) }) async with Layout(Root()) as layout: await layout.render() await layout.deliver(LayoutEvent(event_handler.target, [])) assert did_trigger.current did_trigger.current = False set_event_name.current("second") await layout.render() await layout.deliver(LayoutEvent(event_handler.target, [])) assert did_trigger.current did_trigger.current = False
def model_stream(ws: WebSocket) -> None: def send(value: Any) -> None: ws.send(json.dumps(value)) def recv() -> Optional[LayoutEvent]: event = ws.receive() if event is not None: return LayoutEvent(**json.loads(event)) else: return None query_params = { k: v if len(v) > 1 else v[0] for k, v in parse_query_string( ws.environ["QUERY_STRING"]).items() } run_dispatcher_in_thread( lambda: self._dispatcher_type( Layout(self._root_component_constructor(**query_params))), send, recv, None, )
async def test_shared_state_dispatcher(): done = asyncio.Event() changes_1 = [] changes_2 = [] target_id = "an-event" events_to_inject = [LayoutEvent(target=target_id, data=[])] * 4 async def send_1(patch): changes_1.append(patch.changes) async def recv_1(): await asyncio.sleep(0) try: return events_to_inject.pop(0) except IndexError: done.set() raise asyncio.CancelledError() async def send_2(patch): changes_2.append(patch.changes) async def recv_2(): await done.wait() raise asyncio.CancelledError() @idom.component def Clickable(): count, set_count = idom.hooks.use_state(0) @idom.event(target_id=target_id) async def an_event(): set_count(count + 1) return idom.html.div({"anEvent": an_event, "count": count}) async with SharedViewDispatcher(Layout(Clickable())) as dispatcher: await dispatcher.run(send_1, recv_1, "1") await dispatcher.run(send_2, recv_2, "2") expected_changes = [ [ { "op": "add", "path": "/eventHandlers", "value": { "anEvent": { "target": "an-event", "preventDefault": False, "stopPropagation": False, } }, }, { "op": "add", "path": "/attributes", "value": { "count": 0 } }, { "op": "add", "path": "/tagName", "value": "div" }, ], [{ "op": "replace", "path": "/attributes/count", "value": 1 }], [{ "op": "replace", "path": "/attributes/count", "value": 2 }], [{ "op": "replace", "path": "/attributes/count", "value": 3 }], ] for c_2, expected_c in zip(changes_2, expected_changes): assert_same_items(c_2, expected_c) assert changes_1 == changes_2
def _make_dispatcher(self, params: Dict[str, Any]) -> AbstractDispatcher: return self._dispatcher_type( Layout(self._root_component_constructor(**params)))
class IDOM(PaneBase): priority = None _updates = True _unpack = True _bokeh_model = _BkIDOM def __init__(self, object=None, **params): super().__init__(object, **params) self._idom_loop = None self._idom_model = {} self.param.watch(self._update_layout, 'object') def _update_layout(self, *args): self._idom_model = {} if self._idom_loop is None: return self._setup() def _setup(self): if self.object is None: return from idom.core.component import Component from idom.core.layout import Layout if isinstance(self.object, Layout): self._idom_layout = self.object elif isinstance(self.object, Component): self._idom_layout = Layout(self.object) else: self._idom_layout = Layout(self.object()) self._idom_loop = _spawn_threaded_event_loop(self._idom_layout_render_loop()) def _get_model(self, doc, root=None, parent=None, comm=None): from idom.core.layout import LayoutUpdate if comm: url = '/panel_dist/idom/build/web_modules' else: url = '/'+LOCAL_DIST+'idom/build/web_modules' if self._idom_loop is None: self._setup() update = LayoutUpdate.create_from({}, self._idom_model) props = self._init_params() model = self._bokeh_model( event=[update.path, update.changes], importSourceUrl=url, **props ) if root is None: root = model self._link_props(model, ['msg'], doc, root, comm) if root is None: root = model self._models[root.ref['id']] = (model, parent) return model def _cleanup(self, root): super()._cleanup(root) if not self._models: # Clean up loop when no views are shown try: self._idom_loop.stop() finally: self._idom_loop = None self._idom_layout = None def _process_property_change(self, msg): if msg['msg'] is None: return {} from idom.core.layout import LayoutEvent dispatch = self._idom_layout.dispatch(LayoutEvent(**msg['msg'])) asyncio.run_coroutine_threadsafe(dispatch, loop=self._idom_loop) for ref, (m, _) in self._models.items(): m.msg = None push_on_root(ref) return {} async def _idom_layout_render_loop(self): async with self._idom_layout: while True: update = await self._idom_layout.render() self._idom_model = update.apply_to(self._idom_model) for ref, (model, _) in self._models.items(): doc = state._views[ref][2] if doc.session_context: doc.add_next_tick_callback(partial(model.update, event=update)) else: model.event = update push_on_root(ref) @classmethod def applies(self, object): from idom.core.component import Component from idom.core.layout import Layout if 'idom' in sys.modules: if isinstance(object, (Component, Layout)): return 0.8 elif callable(object): return None return False @classmethod def install(cls, packages, ignore_installed=False, fallback=None): """ Installs specified packages into application directory. Arguments --------- packages: list or tuple The packages to install from npm ignored_installed: boolean Whether to ignore if the package was previously installed. fallback: str or idom.component The fallback to display while the component is loading """ import idom import idom.client.manage idom.client.manage.APP_DIR = DIST_DIR / 'idom' idom.client.manage.BUILD_DIR = DIST_DIR / 'idom' / 'build' idom.client.manage.WEB_MODULES_DIR = DIST_DIR / 'idom' / 'build' / 'web_modules' return idom.install(packages, ignore_installed, fallback) @classmethod def use_param(cls, parameter): """ Links parameter to some IDOM state value and returns the linked value. Arguments --------- parameter: param.Parameter The parameter to link to a idom state value. Returns ------- An idom state value which is updated when the parameter changes. """ import idom from ..depends import param_value_if_widget parameter = param_value_if_widget(parameter) initial = getattr(parameter.owner, parameter.name) value, set_value = idom.hooks.use_state(initial) def update(event): set_value(event.new) parameter.owner.param.watch(update, parameter.name) return value
class IDOM(PaneBase): priority = None _updates = True _unpack = True _bokeh_model = _BkIDOM def __init__(self, object=None, **params): from idom import __version__ as idom_version if Version(_IDOM_MIN_VER) > Version(idom_version) >= Version( _IDOM_MAX_VER): raise RuntimeError( f"Expected idom>={_IDOM_MIN_VER},<{_IDOM_MAX_VER}, but found {idom_version}" ) super().__init__(object, **params) self._idom_loop = None self._idom_model = {} self.param.watch(self._update_layout, 'object') def _update_layout(self, *args): self._idom_model = {} if self._idom_loop is None: return self._setup() def _setup(self): if self.object is None: return from idom.core.component import Component from idom.core.layout import Layout if isinstance(self.object, Layout): self._idom_layout = self.object elif isinstance(self.object, Component): self._idom_layout = Layout(self.object) else: self._idom_layout = Layout(self.object()) self._idom_loop = _spawn_threaded_event_loop( self._idom_layout_render_loop()) def _get_model(self, doc, root=None, parent=None, comm=None): from idom.core.layout import LayoutUpdate from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL # let the client determine import source location IDOM_CLIENT_IMPORT_SOURCE_URL.set("./") if comm: url = '/panel_dist/idom/build' else: url = '/' + LOCAL_DIST + 'idom/build' if self._idom_loop is None: self._setup() update = LayoutUpdate.create_from({}, self._idom_model) props = self._init_params() model = self._bokeh_model(event=[update.path, update.changes], importSourceUrl=url, **props) if root is None: root = model self._link_props(model, ['msg'], doc, root, comm) if root is None: root = model self._models[root.ref['id']] = (model, parent) return model def _cleanup(self, root): super()._cleanup(root) if not self._models: # Clean up loop when no views are shown try: self._idom_loop.stop() finally: self._idom_loop = None self._idom_layout = None def _process_property_change(self, msg): if msg['msg'] is None: return {} from idom.core.layout import LayoutEvent dispatch = self._idom_layout.dispatch(LayoutEvent(**msg['msg'])) asyncio.run_coroutine_threadsafe(dispatch, loop=self._idom_loop) for ref, (m, _) in self._models.items(): m.msg = None push_on_root(ref) return {} async def _idom_layout_render_loop(self): async with self._idom_layout: while True: update = await self._idom_layout.render() self._idom_model = update.apply_to(self._idom_model) for ref, (model, _) in self._models.items(): doc = state._views[ref][2] if doc.session_context: doc.add_next_tick_callback( partial(model.update, event=update)) else: model.event = update push_on_root(ref) @classmethod def applies(self, object): from idom.core.component import Component from idom.core.layout import Layout if 'idom' in sys.modules: if isinstance(object, (Component, Layout)): return 0.8 elif callable(object): return None return False @classmethod def install(cls, packages, ignore_installed=False, fallback=None): """ Installs specified packages into application directory. Arguments --------- packages: list or tuple The packages to install from npm ignored_installed: boolean Whether to ignore if the package was previously installed. fallback: str or idom.component The fallback to display while the component is loading """ import idom from idom.config import IDOM_CLIENT_BUILD_DIR idom_dist_dir = DIST_DIR / "idom" idom_build_dir = idom_dist_dir / "build" if not idom_build_dir.is_dir(): idom_build_dir.mkdir() shutil.copyfile(idom_dist_dir / 'package.json', idom_build_dir / 'package.json') if IDOM_CLIENT_BUILD_DIR.get() != idom_build_dir: IDOM_CLIENT_BUILD_DIR.set(idom_build_dir) # just in case packages were already installed but the build hasn't been # copied over to DIST_DIR yet. ignore_installed = True return idom.install(packages, ignore_installed, fallback) @classmethod def use_param(cls, parameter): """ Links parameter to some IDOM state value and returns the linked value. Arguments --------- parameter: param.Parameter The parameter to link to a idom state value. Returns ------- An idom state value which is updated when the parameter changes. """ import idom from ..depends import param_value_if_widget parameter = param_value_if_widget(parameter) initial = getattr(parameter.owner, parameter.name) value, set_value = idom.hooks.use_state(initial) def update(event): set_value(event.new) parameter.owner.param.watch(update, parameter.name) return value
async def test_dispatch(): events, expected_model = make_events_and_expected_model() changes, send, recv = make_send_recv_callbacks(events) await asyncio.wait_for(serve_json_patch(Layout(Counter()), send, recv), 1) assert_changes_produce_expected_model(changes, expected_model)