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
def use_debug_value( message: Any | Callable[[], Any], dependencies: Sequence[Any] | ellipsis | None = ..., ) -> None: """Log debug information when the given message changes. .. note:: This hook only logs if :data:`~idom.config.IDOM_DEBUG_MODE` is active. Unlike other hooks, a message is considered to have changed if the old and new values are ``!=``. Because this comparison is performed on every render of the component, it may be worth considering the performance cost in some situations. Parameters: message: The value to log or a memoized function for generating the value. dependencies: Dependencies for the memoized function. The message will only be recomputed if the identity of any value in the given sequence changes (i.e. their :func:`id` is different). By default these are inferred based on local variables that are referenced by the given function. """ if not IDOM_DEBUG_MODE.current: return # pragma: no cover old: Ref[Any] = _use_const(lambda: Ref(object())) memo_func = message if callable(message) else lambda: message new = use_memo(memo_func, dependencies) if old.current != new: old.current = new logger.debug(f"{current_hook().component} {new}")
def use_ref(initial_value: _StateType) -> Ref[_StateType]: """See the full :ref:`use_state` docs for details Parameters: initial_value: The value initially assigned to the reference. Returns: A :class:`Ref` object. """ return _use_const(lambda: Ref(initial_value))
def _new_root_model_state( component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]) -> _ModelState: return _ModelState( parent=None, index=-1, key=None, model=Ref(), patch_path="", children_by_key={}, targets_by_event={}, life_cycle_state=_make_life_cycle_state(component, schedule_render), )
def _make_element_model_state( parent: _ModelState, index: int, key: Any, ) -> _ModelState: return _ModelState( parent=parent, index=index, key=key, model=Ref(), patch_path=f"{parent.patch_path}/children/{index}", children_by_key={}, targets_by_event={}, )
def _update_element_model_state( old_model_state: _ModelState, new_parent: _ModelState, new_index: int, ) -> _ModelState: return _ModelState( parent=new_parent, index=new_index, key=old_model_state.key, model=Ref(), # does not copy the model patch_path=old_model_state.patch_path, children_by_key={}, targets_by_event={}, )
async def test_script_from_src(display: DisplayFixture): incr_src_id = Ref() file_name_template = "__some_js_script_{src_id}__.js" @component def HasScript(): src_id, incr_src_id.current = use_counter(0) if src_id == 0: # on initial display we haven't added the file yet. return html.div() else: return html.div( html.div({"id": "run-count", "data-value": 0}), html.script( {"src": f"/_api/modules/{file_name_template.format(src_id=src_id)}"} ), ) await display.show(HasScript) for i in range(1, 4): script_file = config.IDOM_WEB_MODULES_DIR.current / file_name_template.format( src_id=i ) script_file.write_text( f""" let runCountEl = document.getElementById("run-count"); runCountEl.setAttribute("data-value", {i}); """ ) incr_src_id.current() run_count = await display.page.wait_for_selector("#run-count", state="attached") poll_run_count = poll(run_count.get_attribute, "data-value") await poll_run_count.until_equals("1")
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
def _make_component_model_state( parent: _ModelState, index: int, key: Any, component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None], ) -> _ModelState: return _ModelState( parent=parent, index=index, key=key, model=Ref(), patch_path=f"{parent.patch_path}/children/{index}", children_by_key={}, targets_by_event={}, life_cycle_state=_make_life_cycle_state(component, schedule_render), )
def _copy_component_model_state(old_model_state: _ModelState) -> _ModelState: # use try/except here because not having a parent is rare (only the root state) try: parent: Optional[_ModelState] = old_model_state.parent except AttributeError: parent = None return _ModelState( parent=parent, index=old_model_state.index, key=old_model_state.key, model=Ref(), # does not copy the model patch_path=old_model_state.patch_path, children_by_key={}, targets_by_event={}, life_cycle_state=old_model_state.life_cycle_state, )
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)
def _update_component_model_state( old_model_state: _ModelState, new_parent: _ModelState, new_index: int, new_component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None], ) -> _ModelState: return _ModelState( parent=new_parent, index=new_index, key=old_model_state.key, model=Ref(), # does not copy the model patch_path=old_model_state.patch_path, children_by_key={}, targets_by_event={}, life_cycle_state=(_update_life_cycle_state( old_model_state.life_cycle_state, new_component) if old_model_state.is_component_state else _make_life_cycle_state(new_component, schedule_render)), )
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
async def serve_development_app( app: Flask, host: str, port: int, started: asyncio.Event | None = None, ) -> None: """Run an application using a development server""" loop = asyncio.get_event_loop() stopped = asyncio.Event() server: Ref[BaseWSGIServer] = Ref() def run_server() -> None: server.current = make_server(host, port, app, threaded=True) if started: loop.call_soon_threadsafe(started.set) try: server.current.serve_forever() # type: ignore finally: loop.call_soon_threadsafe(stopped.set) thread = Thread(target=run_server, daemon=True) thread.start() if started: await started.wait() try: await stopped.wait() finally: # we may have exitted because this task was cancelled server.current.shutdown() # the thread should eventually join thread.join(timeout=3) # just double check it happened if thread.is_alive(): # pragma: no cover raise RuntimeError("Failed to shutdown server.")
async def test_script_re_run_on_content_change(display: DisplayFixture): incr_count = Ref() @component def HasScript(): count, incr_count.current = use_counter(1) return html.div( html.div({"id": "mount-count", "data-value": 0}), html.div({"id": "unmount-count", "data-value": 0}), html.script( f"""() => {{ const mountCountEl = document.getElementById("mount-count"); const unmountCountEl = document.getElementById("unmount-count"); mountCountEl.setAttribute("data-value", {count}); return () => unmountCountEl.setAttribute("data-value", {count});; }}""" ), ) await display.show(HasScript) mount_count = await display.page.wait_for_selector("#mount-count", state="attached") poll_mount_count = poll(mount_count.get_attribute, "data-value") unmount_count = await display.page.wait_for_selector( "#unmount-count", state="attached" ) poll_unmount_count = poll(unmount_count.get_attribute, "data-value") await poll_mount_count.until_equals("1") await poll_unmount_count.until_equals("0") incr_count.current() await poll_mount_count.until_equals("2") await poll_unmount_count.until_equals("1") incr_count.current() await poll_mount_count.until_equals("3") await poll_unmount_count.until_equals("2")
async def test_script_mount_unmount(display: DisplayFixture): toggle_is_mounted = Ref() @component def Root(): is_mounted, toggle_is_mounted.current = use_toggle() return html.div( html.div({"id": "mount-state", "data-value": False}), HasScript() if is_mounted else html.div(), ) @component def HasScript(): return html.script( """() => { const mapping = {"false": false, "true": true}; const mountStateEl = document.getElementById("mount-state"); mountStateEl.setAttribute( "data-value", !mapping[mountStateEl.getAttribute("data-value")]); return () => mountStateEl.setAttribute( "data-value", !mapping[mountStateEl.getAttribute("data-value")]); }""" ) await display.show(Root) mount_state = await display.page.wait_for_selector("#mount-state", state="attached") poll_mount_state = poll(mount_state.get_attribute, "data-value") await poll_mount_state.until_equals("true") toggle_is_mounted.current() await poll_mount_state.until_equals("false") toggle_is_mounted.current() await poll_mount_state.until_equals("true")
def hotswap(shared: bool = False) -> Tuple[MountFunc, ComponentConstructor]: """Swap out components from a layout on the fly. Since you can't change the component functions used to create a layout in an imperative manner, you can use ``hotswap`` to do this so long as you set things up ahead of time. Parameters: shared: Whether or not all views of the layout should be udpated on a swap. Example: .. code-block:: python import idom show, root = idom.hotswap() PerClientState(root).daemon("localhost", 8765) @idom.component def DivOne(self): return {"tagName": "div", "children": [1]} show(DivOne) # displaying the output now will show DivOne @idom.component def DivTwo(self): return {"tagName": "div", "children": [2]} show(DivTwo) # displaying the output now will show DivTwo """ constructor_ref: Ref[Callable[[], Any]] = Ref(lambda: {"tagName": "div"}) if shared: set_constructor_callbacks: Set[Callable[[Callable[[], Any]], None]] = set() @component def HotSwap() -> Any: # new displays will adopt the latest constructor and arguments constructor, set_constructor = _use_callable(constructor_ref.current) def add_callback() -> Callable[[], None]: set_constructor_callbacks.add(set_constructor) return lambda: set_constructor_callbacks.remove(set_constructor) hooks.use_effect(add_callback) return constructor() def swap(constructor: Callable[[], Any]) -> None: constructor_ref.current = constructor for set_constructor in set_constructor_callbacks: set_constructor(constructor) return None else: @component def HotSwap() -> Any: return constructor_ref.current() def swap(constructor: Callable[[], Any]) -> None: constructor_ref.current = constructor return None return swap, HotSwap