예제 #1
0
 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))
예제 #2
0
 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())
예제 #3
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
예제 #4
0
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]
예제 #5
0
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})"
예제 #6
0
파일: sanic.py 프로젝트: idom-team/idom
 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,
     )
예제 #7
0
 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}")
예제 #8
0
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
예제 #9
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)
예제 #10
0
파일: tornado.py 프로젝트: idom-team/idom
    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,
            ))
예제 #11
0
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]
예제 #12
0
    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
예제 #13
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
예제 #14
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
예제 #15
0
        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,
            )
예제 #16
0
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
예제 #17
0
 def _make_dispatcher(self, params: Dict[str, Any]) -> AbstractDispatcher:
     return self._dispatcher_type(
         Layout(self._root_component_constructor(**params)))
예제 #18
0
파일: idom.py 프로젝트: rmorshea/panel
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
예제 #19
0
파일: idom.py 프로젝트: brl0/panel
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
예제 #20
0
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)