From c0a3902f096a8a5e18bc945b663b1df5d24f5620 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 25 Jan 2023 17:56:23 +0100 Subject: [PATCH] Remove IDOM (#4322) --- .github/workflows/docs.yaml | 2 +- examples/reference/panes/IDOM.ipynb | 174 ---------------- panel/models/__init__.py | 1 - panel/models/idom.py | 14 -- panel/models/idom.ts | 304 ---------------------------- panel/package.json | 1 - panel/pane/__init__.py | 2 - panel/pane/idom.py | 232 --------------------- panel/tests/pane/test_base.py | 4 +- setup.py | 2 +- 10 files changed, 4 insertions(+), 732 deletions(-) delete mode 100644 examples/reference/panes/IDOM.ipynb delete mode 100644 panel/models/idom.py delete mode 100644 panel/models/idom.ts delete mode 100644 panel/pane/idom.py diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2a9fab40d7..b706341060 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -70,7 +70,7 @@ jobs: run: | conda activate test-environment doit develop_install -o examples -o doc - pip install --use-deprecated=legacy-resolver pyecharts "idom==0.24" "pydata-sphinx-theme<=0.9.0" "pydata-sphinx-theme<=0.9.0" sphinx-copybutton sphinx-design + pip install --use-deprecated=legacy-resolver pyecharts "pydata-sphinx-theme<=0.9.0" "pydata-sphinx-theme<=0.9.0" sphinx-copybutton sphinx-design - name: doit env_capture run: | conda activate test-environment diff --git a/examples/reference/panes/IDOM.ipynb b/examples/reference/panes/IDOM.ipynb deleted file mode 100644 index dd8fde006e..0000000000 --- a/examples/reference/panes/IDOM.ipynb +++ /dev/null @@ -1,174 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import idom\n", - "\n", - "import panel as pn\n", - "\n", - "pn.extension()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**WARNING**: Currently Panel only support idom version 0.24. We are working on supporting later version as part of the [idom-bokeh project](https://github.com/idom-team/idom-bokeh/).\n", - "\n", - "The ``IDOM`` pane renders any [IDOM component](http://idom-docs.herokuapp.com/) both in the notebook and in a deployed server. IDOM defines an API for defining and controlling interactive HTML components directly from Python. Note that in the notebook the IDOM support for loading external modules relies on Panel's Jupyter serverextension. To check if this is enabled you can run:\n", - "\n", - " jupyter serverextension list\n", - " \n", - "You should see:\n", - "\n", - " panel.io.jupyter_server_extension enabled\n", - " - Validating...\n", - " panel.io.jupyter_server_extension OK\n", - "\n", - "If you don't see this but have installed panel you can manually enable the server extension with:\n", - "\n", - " jupyter serverextension enable --sys-prefix panel.io.jupyter_server_extension\n", - " \n", - "which will install it where your Python is installed or to place it in the Jupyter config in your home directory:\n", - "\n", - " jupyter serverextension enable --py panel\n", - "\n", - "#### Parameters:\n", - "\n", - "For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n", - "\n", - "* **``object``** (object): The IDOM component being displayed\n", - "\n", - "##### Display\n", - "\n", - "* **``default_layout``** (pn.layout.Panel, default=Row): Layout to wrap the plot and widgets in\n", - "\n", - "___" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `panel` function will automatically convert any ``idom.component`` into a displayable panel, while keeping all of its interactive features:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@idom.component\n", - "def ClickCount():\n", - " count, set_count = idom.hooks.use_state(0)\n", - "\n", - " return idom.html.button(\n", - " {\"onClick\": lambda event: set_count(count + 1)},\n", - " [f\"Click count: {count}\"],\n", - " )\n", - "\n", - "pn.pane.IDOM(ClickCount, width=300)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This makes it possible to generate even complex interactive components directly from Python, e.g. here we will create a ToDo list:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@idom.component\n", - "def Todo():\n", - " items, set_items = idom.hooks.use_state([])\n", - "\n", - " async def add_new_task(event):\n", - " if event[\"key\"] == \"Enter\":\n", - " set_items(items + [event[\"value\"]])\n", - "\n", - " tasks = []\n", - "\n", - " for index, text in enumerate(items):\n", - "\n", - " async def remove_task(event, index=index):\n", - " set_items(items[:index] + items[index + 1 :])\n", - "\n", - " task_text = idom.html.td(idom.html.p(text))\n", - " delete_button = idom.html.td({\"onClick\": remove_task}, idom.html.button([\"x\"]))\n", - " tasks.append(idom.html.tr(task_text, delete_button))\n", - "\n", - " task_input = idom.html.input({\"onKeyDown\": add_new_task})\n", - " task_table = idom.html.table(tasks)\n", - "\n", - " return idom.html.div(task_input, task_table)\n", - "\n", - "pn.pane.IDOM(Todo)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have a live server backing your session, whether that is a notebook server or a Bokeh/Panel server deployment you can also use external Javascript components which will be compiled before first use. See the [idom documentation](https://idom-docs.herokuapp.com/docs/javascript-components.html) for more details on using external components. Note that to ensure that the JS modules are installed in the correct place you should use `pn.pane.IDOM.install` rather than simply using `idom.install`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "victory = pn.pane.IDOM.install(\"victory\", fallback=\"loading...\")\n", - "\n", - "victory_com = idom.component(\n", - " lambda: victory.VictoryBar({\"style\": {\"parent\": {\"width\": \"500px\"}}}),\n", - ")\n", - "\n", - "pn.pane.IDOM(victory_com)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to work with Panel components seamlessly the `IDOM` pane also provides a `use_param` method which allows us to use the current parameter value much like we would when using `pn.depends`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "aw = pn.widgets.IntSlider(name='a', start=0, end=20, value=1)\n", - "bw = pn.widgets.IntSlider(name='b', start=0, end=20, value=1)\n", - "\n", - "@idom.component\n", - "def view():\n", - " a = pn.pane.IDOM.use_param(aw)\n", - " b = pn.pane.IDOM.use_param(bw.param.value) # equivalent to passing in the widget\n", - " return idom.html.div({}, f'{a}+{b}={a+b}')\n", - "\n", - "pn.Row(aw, bw, view)" - ] - } - ], - "metadata": { - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 2952785651..fef419df40 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -6,7 +6,6 @@ """ from .datetime_picker import DatetimePicker # noqa -from .idom import IDOM # noqa from .ipywidget import IPyWidget # noqa from .layout import Card # noqa from .location import Location # noqa diff --git a/panel/models/idom.py b/panel/models/idom.py deleted file mode 100644 index 445ef7b551..0000000000 --- a/panel/models/idom.py +++ /dev/null @@ -1,14 +0,0 @@ -from bokeh.core.properties import ( - Any, Dict, Either, Null, String, Tuple, -) - -from .layout import HTMLBox - - -class IDOM(HTMLBox): - - importSourceUrl = String() - - event = Tuple(Any, Any) - - msg = Either(Dict(String, Any), Null) diff --git a/panel/models/idom.ts b/panel/models/idom.ts deleted file mode 100644 index a7fc2bdbe1..0000000000 --- a/panel/models/idom.ts +++ /dev/null @@ -1,304 +0,0 @@ -import {render, createContext} from 'preact'; -import {useState, useEffect, useContext, useRef, useCallback} from 'preact/hooks'; -import {html} from 'htm/preact'; -import {applyPatch, getValueByPointer} from 'fast-json-patch'; - -import * as p from "@bokehjs/core/properties" -import {HTMLBox, HTMLBoxView} from "./layout" -import {serializeEvent} from "./event-to-object" - -const LayoutConfigContext = createContext({}); - -export function mountLayout( - mountElement: any, - saveUpdateHook: any, - sendEvent: any, - importSourceUrl: string -) { - render( - html` - <${Layout} - saveUpdateHook=${saveUpdateHook} - sendEvent=${sendEvent} - importSourceUrl=${importSourceUrl} - /> - `, - mountElement - ) -} - -export default function Layout({ saveUpdateHook, sendEvent, importSourceUrl }: { saveUpdateHook: any, sendEvent: any, importSourceUrl: string } ) { - const [model, patchModel] = useInplaceJsonPatch({}); - - useEffect(() => saveUpdateHook(patchModel), [patchModel]); - - if (model.tagName) { - return html` - <${LayoutConfigContext.Provider} - value=${{ - sendEvent: sendEvent, - importSourceUrl: importSourceUrl, - }} - > - <${Element} model=${model} /> - - ` - } else - return html`
` -} - -function Element({ model }: { model: any }) { - if (model.importSource) - return html`<${ImportedElement} model=${model} />` - else - return html`<${StandardElement} model=${model} />` -} - -function ImportedElement({ model }: { model: any }) { - const config: any = useContext(LayoutConfigContext); - const module = useLazyModule( - model.importSource.source, - config.importSourceUrl - ) - if (module) { - const cmpt = getPathProperty(module, model.tagName); - const children = elementChildren(model); - const attributes = elementAttributes(model, config.sendEvent); - return html`<${cmpt} ...${attributes}>${children}`; - } else { - const fallback = model.importSource.fallback; - if (!fallback) - return html`
` - switch (typeof fallback) { - case "object": - return html`<${Element} model=${fallback} />` - case "string": - return html`
${fallback}
` - default: - return null - } - } -} - -function StandardElement({ model }: { model: any }) { - const config: any = useContext(LayoutConfigContext); - const children = elementChildren(model); - const attributes = elementAttributes(model, config.sendEvent); - if (model.children && model.children.length) - return html`<${model.tagName} ...${attributes}>${children}` - else - return html`<${model.tagName} ...${attributes} />` -} - -function elementChildren(model: any) { - if (!model.children) - return [] - else { - return model.children.map((child: any) => { - switch (typeof child) { - case "object": - return html`<${Element} model=${child} />` - case "string": - return child - default: - return null - } - }); - } -} - -function elementAttributes(model: any, sendEvent: any) { - const attributes = Object.assign({}, model.attributes) - - if (model.eventHandlers) { - Object.keys(model.eventHandlers).forEach((eventName) => { - const eventSpec = model.eventHandlers[eventName] - attributes[eventName] = eventHandler(sendEvent, eventSpec) - }) - } - - return attributes -} - -function eventHandler(sendEvent: any, eventSpec: any) { - return function (): Promise { - const data = Array.from(arguments).map((value) => { - if (typeof value === "object") { - if (eventSpec["preventDefault"]) - value.preventDefault(); - if (eventSpec["stopPropagation"]) - value.stopPropagation() - return serializeEvent(value) - } else - return value - }); - return new Promise((resolve: any) => { - const msg = { - data: data, - target: eventSpec["target"], - } - sendEvent(msg) - resolve(msg) - }) - } -} - -function useLazyModule(source: string, sourceUrlBase: string = "") { - const [module, setModule] = useState(null); - // use eval() to avoid weird build behavior by bundlers like Webpack - if (!module) - eval(`import('${joinUrl(sourceUrlBase, source)}')`).then(setModule) - return module -} - -function getPathProperty(obj: any, prop: string) { - // properties may be dot seperated strings - const path = prop.split(".") - const firstProp: any = path.shift() - let value = obj[firstProp] - for (let i = 0; i < path.length; i++) - value = value[path[i]] - return value -} - -function useInplaceJsonPatch(doc: any) { - const ref = useRef(doc); - const forceUpdate = useForceUpdate(); - - const applyPatch = useCallback( - (path: any, patch: any) => { - applyPatchInplace(ref.current, path, patch) - forceUpdate() - }, - [ref, forceUpdate] - ) - - return [ref.current, applyPatch]; -} - -function applyPatchInplace(doc: any, path: any, patch: any) { - if (!path) - applyPatch(doc, patch) - else { - applyPatch(doc, [ - { - op: "replace", - path: path, - value: applyPatch( - getValueByPointer(doc, path), - patch, - false, - false - ).newDocument, - }, - ]) - } -} - -function useForceUpdate() { - const [, updateState] = useState({}) - return useCallback(() => updateState({}), []) -} - -function joinUrl(base: string, tail: string) { - return tail.startsWith("./") - ? (base.endsWith("/") ? base.slice(0, -1) : base) + tail.slice(1) - : tail; -} - -export class IDOMView extends HTMLBoxView { - model: IDOM - _update: any - - connect_signals(): void { - super.connect_signals() - this.connect(this.model.properties.event.change, () => { - this._update(...this.model.event) - setTimeout(() => { requestAnimationFrame(() => this.fix_layout()) }) - }) - } - - fix_layout(): void { - this.update_layout() - this.compute_layout() - this.invalidate_layout() - //set_size(this.el, this.model) - } - - initialize(): void { - super.initialize() - mountLayout( - this.el, - (update: any) => this._save_update(update), - (event: any) => this._send(event), - this.model.importSourceUrl - ) - } - - async lazy_initialize(): Promise { - await super.lazy_initialize() - await new Promise((resolve) => { - const check_update = () => { - if (this._update) - resolve(null) - else - setTimeout(check_update, 100); - } - check_update() - }) - } - - _save_update(update: any): any { - this._update = update - } - - async render(): Promise { - super.render() - this._update(...this.model.event) - await new Promise((resolve) => { - const check_update = () => { - if (this.el.children.length) { - this.fix_layout() - resolve(null) - } else - setTimeout(check_update, 50) - } - check_update() - }) - } - - _send(event: any): any { - this.model.msg = event - } -} - -export namespace IDOM { - export type Attrs = p.AttrsOf - - export type Props = HTMLBox.Props & { - event: p.Property - importSourceUrl: p.Property - msg: p.Property - } -} - -export interface IDOM extends IDOM.Attrs {} - -export class IDOM extends HTMLBox { - properties: IDOM.Props - - constructor(attrs?: Partial) { - super(attrs) - } - - static __module__ = "panel.models.idom" - - static { - this.prototype.default_view = IDOMView - this.define(({Any, String}) => ({ - event: [ Any, [] ], - importSourceUrl: [ String, '' ], - msg: [ Any, {} ], - })) - } -} diff --git a/panel/package.json b/panel/package.json index a48f6f13f6..b5d3d96f25 100644 --- a/panel/package.json +++ b/panel/package.json @@ -13,7 +13,6 @@ "@types/debounce": "^1.2.0", "@types/gl-matrix": "^2.4.5", "debounce": "^1.2.0", - "fast-json-patch": "^2.2.1", "gl-matrix": "^3.1.0", "htm": "^3.0.4", "json-formatter-js": "^2.2.1", diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py index 3e8d699230..6bcbdafdb2 100644 --- a/panel/pane/__init__.py +++ b/panel/pane/__init__.py @@ -37,7 +37,6 @@ from .echarts import ECharts # noqa from .equation import LaTeX # noqa from .holoviews import HoloViews, Interactive # noqa -from .idom import IDOM # noqa from .image import ( # noqa GIF, ICO, JPG, PDF, PNG, SVG, ) @@ -66,7 +65,6 @@ "HoloViews", "HTML", "ICO", - "IDOM", "Interactive", "IPyWidget", "IPyLeaflet", diff --git a/panel/pane/idom.py b/panel/pane/idom.py deleted file mode 100644 index 86c06ff009..0000000000 --- a/panel/pane/idom.py +++ /dev/null @@ -1,232 +0,0 @@ -from __future__ import annotations - -import asyncio -import shutil -import sys - -from functools import partial -from queue import Queue as SyncQueue -from threading import Thread -from typing import TYPE_CHECKING, Optional - -from packaging.version import Version - -from ..io.notebook import push_on_root -from ..io.resources import DIST_DIR, LOCAL_DIST -from ..io.state import state -from ..models import IDOM as _BkIDOM -from .base import PaneBase - -if TYPE_CHECKING: - from bokeh.document import Document - from bokeh.model import Model - from pyviz_comms import Comm - -_IDOM_MIN_VER = "0.23" -_IDOM_MAX_VER = "0.24" - - -def _spawn_threaded_event_loop(coro): - loop_q = SyncQueue() - - def run_in_thread(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop_q.put(loop) - loop.run_until_complete(coro) - - thread = Thread(target=run_in_thread, daemon=True) - thread.start() - - return loop_q.get() - - -class IDOM(PaneBase): - """ - The `IDOM` pane renders any IDOM component both in the notebook and in a - deployed server. - - IDOM defines an API for defining and controlling interactive HTML - components directly from Python. - - Note that in the notebook the IDOM support for loading external modules - relies on Panel’s Jupyter serverextension. - - Reference: https://panel.holoviz.org/reference/panes/IDOM.html - - :Example: - - >>> IDOM(ClickCount, width=300) - """ - - 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: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None - ) -> Model: - from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL - from idom.core.layout import LayoutUpdate - - # 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: Model | None = None) -> None: - 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): - if object is None: - return None - elif 'idom' in sys.modules: - from idom.core.component import Component - from idom.core.layout import Layout - 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 diff --git a/panel/tests/pane/test_base.py b/panel/tests/pane/test_base.py index a294d9a7f5..9d022cfc7a 100644 --- a/panel/tests/pane/test_base.py +++ b/panel/tests/pane/test_base.py @@ -7,12 +7,12 @@ from panel.layout import Row from panel.links import CallbackGenerator from panel.pane import ( - IDOM, Bokeh, HoloViews, Interactive, IPyWidget, PaneBase, Vega, + Bokeh, HoloViews, Interactive, IPyWidget, PaneBase, Vega, ) from panel.param import ParamMethod from panel.tests.util import check_layoutable_properties -SKIP_PANES = (Bokeh, HoloViews, ParamMethod, interactive, IPyWidget, Interactive, IDOM, Vega) +SKIP_PANES = (Bokeh, HoloViews, ParamMethod, interactive, IPyWidget, Interactive, Vega) all_panes = [w for w in param.concrete_descendents(PaneBase).values() if not w.__name__.startswith('_') and not diff --git a/setup.py b/setup.py index ac7a6571e1..f71999eedb 100644 --- a/setup.py +++ b/setup.py @@ -146,7 +146,7 @@ def run(self): 'markdown-it-py', 'ipyvuetify', 'reacton', - # Added lxml temporarily as installing pyechars or idom on Python 3.11 + # Added lxml temporarily as installing pyecharts on Python 3.11 # via pip tries to build it and fails. To be removed. 'lxml', 'numpy <1.24', # Avoid VTK test fail