Exemplo n.º 1
0
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
Exemplo n.º 2
0
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}")
Exemplo n.º 3
0
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))
Exemplo n.º 4
0
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),
    )
Exemplo n.º 5
0
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={},
    )
Exemplo n.º 6
0
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={},
    )
Exemplo n.º 7
0
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")
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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),
    )
Exemplo n.º 10
0
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,
    )
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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)),
    )
Exemplo n.º 13
0
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
Exemplo n.º 14
0
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.")
Exemplo n.º 15
0
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")
Exemplo n.º 16
0
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")
Exemplo n.º 17
0
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