From 642777769c2957620612dd771f99d046ef24de5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dtalo=20Epif=C3=A2nio?= Date: Mon, 19 Feb 2024 10:43:47 -0300 Subject: [PATCH] Add ipywatch --- debugbar/_modidx.py | 8 -- debugbar/core.py | 7 -- {debugbar => ipywatch}/__init__.py | 0 ipywatch/_modidx.py | 34 +++++++ ipywatch/history.py | 50 ++++++++++ ipywatch/ipywatch.py | 67 +++++++++++++ nbs/00_comm.ipynb | 1 - nbs/01_reacton.ipynb | 63 +++---------- nbs/02_widget_history.ipynb | 119 +++++++++++++++++++++++ nbs/03_ipywatch.ipynb | 146 +++++++++++++++++++++++++++++ settings.ini | 8 +- 11 files changed, 435 insertions(+), 68 deletions(-) delete mode 100644 debugbar/_modidx.py delete mode 100644 debugbar/core.py rename {debugbar => ipywatch}/__init__.py (100%) create mode 100644 ipywatch/_modidx.py create mode 100644 ipywatch/history.py create mode 100644 ipywatch/ipywatch.py create mode 100644 nbs/02_widget_history.ipynb create mode 100644 nbs/03_ipywatch.ipynb diff --git a/debugbar/_modidx.py b/debugbar/_modidx.py deleted file mode 100644 index ce9fcae..0000000 --- a/debugbar/_modidx.py +++ /dev/null @@ -1,8 +0,0 @@ -# Autogenerated by nbdev - -d = { 'settings': { 'branch': 'main', - 'doc_baseurl': '/debugbar', - 'doc_host': 'https://itepifanio.github.io', - 'git_url': 'https://github.com/itepifanio/debugbar', - 'lib_path': 'debugbar'}, - 'syms': {'debugbar.core': {'debugbar.core.foo': ('core.html#foo', 'debugbar/core.py')}}} diff --git a/debugbar/core.py b/debugbar/core.py deleted file mode 100644 index 6552cc5..0000000 --- a/debugbar/core.py +++ /dev/null @@ -1,7 +0,0 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb. - -# %% auto 0 -__all__ = ['foo'] - -# %% ../nbs/00_core.ipynb 3 -def foo(): pass diff --git a/debugbar/__init__.py b/ipywatch/__init__.py similarity index 100% rename from debugbar/__init__.py rename to ipywatch/__init__.py diff --git a/ipywatch/_modidx.py b/ipywatch/_modidx.py new file mode 100644 index 0000000..77ac69d --- /dev/null +++ b/ipywatch/_modidx.py @@ -0,0 +1,34 @@ +# Autogenerated by nbdev + +d = { 'settings': { 'branch': 'main', + 'doc_baseurl': '/ipywatch', + 'doc_host': 'https://itepifanio.github.io', + 'git_url': 'https://github.com/itepifanio/ipywatch', + 'lib_path': 'ipywatch'}, + 'syms': { 'ipywatch.history': { 'ipywatch.history.WidgetStateHistory': ('widget_history.html#widgetstatehistory', 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__delitem__': ( 'widget_history.html#widgetstatehistory.__delitem__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__getitem__': ( 'widget_history.html#widgetstatehistory.__getitem__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__init__': ( 'widget_history.html#widgetstatehistory.__init__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__iter__': ( 'widget_history.html#widgetstatehistory.__iter__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__len__': ( 'widget_history.html#widgetstatehistory.__len__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.__setitem__': ( 'widget_history.html#widgetstatehistory.__setitem__', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.get_current_state': ( 'widget_history.html#widgetstatehistory.get_current_state', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.get_state_history': ( 'widget_history.html#widgetstatehistory.get_state_history', + 'ipywatch/history.py'), + 'ipywatch.history.WidgetStateHistory.set_state': ( 'widget_history.html#widgetstatehistory.set_state', + 'ipywatch/history.py')}, + 'ipywatch.ipywatch': { 'ipywatch.ipywatch.Ipywatch': ('ipywatch.html#ipywatch', 'ipywatch/ipywatch.py'), + 'ipywatch.ipywatch.Ipywatch.__init__': ('ipywatch.html#ipywatch.__init__', 'ipywatch/ipywatch.py'), + 'ipywatch.ipywatch.Ipywatch._on_state_change': ( 'ipywatch.html#ipywatch._on_state_change', + 'ipywatch/ipywatch.py'), + 'ipywatch.ipywatch.WidgetStateHistoryListener': ( 'ipywatch.html#widgetstatehistorylistener', + 'ipywatch/ipywatch.py'), + 'ipywatch.ipywatch.WidgetStateHistoryListener.__init__': ( 'ipywatch.html#widgetstatehistorylistener.__init__', + 'ipywatch/ipywatch.py')}}} diff --git a/ipywatch/history.py b/ipywatch/history.py new file mode 100644 index 0000000..94a7670 --- /dev/null +++ b/ipywatch/history.py @@ -0,0 +1,50 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_widget_history.ipynb. + +# %% auto 0 +__all__ = ['WidgetStateHistory'] + +# %% ../nbs/02_widget_history.ipynb 3 +from collections import deque +from typing import Any, Deque, Dict, Iterator + +# %% ../nbs/02_widget_history.ipynb 4 +class WidgetStateHistory: + def __init__(self, history_size: int = 5): + self.history_size = history_size + self._widget_states: Dict[str, Any] = {} + + def set_state(self, comm_id: str, state: Any): + if comm_id not in self._widget_states: + self._widget_states[comm_id] = deque(maxlen=self.history_size) + + self._widget_states[comm_id].append(state) + + def get_current_state(self, comm_id: str): + if comm_id in self._widget_states and self._widget_states[comm_id]: + return self._widget_states[comm_id][-1] + + raise KeyError(f"No state found for widget comm_id: {comm_id}") + + def get_state_history(self, comm_id: str) -> Any: + if comm_id in self._widget_states: + return self._widget_states[comm_id] + + raise KeyError(f"No history found for widget comm_id: {comm_id}") + + def __setitem__(self, comm_id: str, state: Any): + self.set_state(comm_id, state) + + def __getitem__(self, comm_id: str): + return self.get_current_state(comm_id) + + def __delitem__(self, comm_id: str): + if comm_id in self._widget_states: + del self._widget_states[comm_id] + else: + raise KeyError(f"Comm ID {comm_id} not found") + + def __iter__(self) -> Iterator[str]: + return iter(self._widget_states) + + def __len__(self) -> int: + return len(self._widget_states) diff --git a/ipywatch/ipywatch.py b/ipywatch/ipywatch.py new file mode 100644 index 0000000..d10fec3 --- /dev/null +++ b/ipywatch/ipywatch.py @@ -0,0 +1,67 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_ipywatch.ipynb. + +# %% auto 0 +__all__ = ['WidgetStateHistoryListener', 'Ipywatch'] + +# %% ../nbs/03_ipywatch.ipynb 3 +from typing import Any, Callable, List + +import ipywidgets +from ipykernel.comm import Comm +from ipywidgets import HBox, VBox, Label, Output, Button, Text, Tab, Layout + +from ipywatch.history import WidgetStateHistory + +# %% ../nbs/03_ipywatch.ipynb 4 +class WidgetStateHistoryListener: + def __init__( + self, + history_size: int = 5, + on_state_change: Callable[[ipywidgets.Widget, Any], None]=None + ): + self.history_size = history_size + self.history = WidgetStateHistory(history_size) + self.on_state_change = on_state_change + + _original_send = Comm.send + + def _patched_send(comm, data=None, metadata=None, buffers=None): + comm_id = comm.comm_id + + widget = ipywidgets.Widget.widgets.get(comm_id) + + self.history.set_state(comm_id, data) + + if self.on_state_change: + self.on_state_change(widget, data) + + _original_send(comm, data, metadata, buffers) + + Comm.send = _patched_send + +# %% ../nbs/03_ipywatch.ipynb 5 +class Ipywatch(HBox): + def __init__(self, width: str = '100%', height: str = '400px', history_size: int = 5, **kwargs): + self.updating = False # Flag to prevent recursion + + self.listener = WidgetStateHistoryListener(history_size=history_size) + + self.messages = Output(layout=dict(width='100%', height='400px')) + + self.listener.on_state_change = self._on_state_change + + super().__init__( + children=[self.messages], + layout=Layout(width=width, height=height) + ) + + def _on_state_change(self, widget, state): + if self.updating: + return + + self.updating = True + + widget_type = type(widget).__name__ if widget else "Unknown" + self.messages.append_stdout(f"Event emitted by {widget_type}: {state}\n") + + self.updating = False diff --git a/nbs/00_comm.ipynb b/nbs/00_comm.ipynb index 82db989..ac32b94 100644 --- a/nbs/00_comm.ipynb +++ b/nbs/00_comm.ipynb @@ -18,7 +18,6 @@ "#| hide\n", "import solara\n", "import ipywidgets as widgets\n", - "\n", "from ipykernel.comm import Comm" ] }, diff --git a/nbs/01_reacton.ipynb b/nbs/01_reacton.ipynb index 4bc548a..b9e269f 100644 --- a/nbs/01_reacton.ipynb +++ b/nbs/01_reacton.ipynb @@ -25,6 +25,14 @@ "from reacton.core import _RenderContext, ComponentContext, Element" ] }, + { + "cell_type": "markdown", + "id": "e72c2232-af05-40ca-8250-b147590d7b75", + "metadata": {}, + "source": [ + "Adding a `pre_render` hook to reacton codebase we're able to track each state change at component level. " + ] + }, { "cell_type": "code", "execution_count": null, @@ -35,7 +43,10 @@ "_original_render = _RenderContext.render\n", "\n", "def pre_render(self, element: Element, container: widgets.Widget = None):\n", - " print(f'pre_render::{self.state_get()} \\n\\n{element.component}\\n\\n{container if container is None else container.model_id}')\n", + " print(f'state::{self.state_get()}\\n')\n", + " print(f'element: {element.component} --- {element.component.name}, {element.component.value_name}, {element.component.widget}\\n')\n", + " if container is not None:\n", + " print(f'model_id::{container.model_id}')\n", "\n", "def _patched_render(self, element: Element, container: widgets.Widget = None):\n", " pre_render(self, element, container)\n", @@ -45,58 +56,14 @@ "_RenderContext.render = _patched_render" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c8fab2f-37d2-4c41-a0fa-c0a41ec4249e", - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - "_original_state_get = _RenderContext.state_get\n", - "_original_state_set = _RenderContext.state_set\n", - "\n", - "def _patched_state_get(self, context: Optional[ComponentContext] = None):\n", - " state = context.state if context is not None else context\n", - " print(f'_patched_state_get::{state}')\n", - "\n", - " _original_state_get(self, context)\n", - "\n", - "def _patched_state_set(self, context: ComponentContext, state):\n", - " print(f'_patched_state_set::{state}')\n", - "\n", - " _original_state_set(self, context, state)\n", - "\n", - "_RenderContext.state_get = _patched_state_get\n", - "_RenderContext.state_set = _patched_state_set\n", - "\"\"\"" - ] - }, { "cell_type": "markdown", "id": "9d112073-92ce-4571-9070-8dd51a89beb1", "metadata": {}, "source": [ - "The following example was adapted from [reacton test suite](https://github.com/widgetti/reacton/blob/3eee8f7681d5aad8d56c284f0c76fb7e55f8d917/reacton/core_test.py#L1982). Patching `state_get` and `state_set` didn't seems to track the widget changes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5f290cc7-2e9b-4a07-963e-33290c663c8c", - "metadata": {}, - "outputs": [], - "source": [ - "@react.component\n", - "def Test():\n", - " value, set_value = react.use_state(0)\n", - " return react_widgets.IntSlider(value=value)\n", + "## Testing\n", "\n", - "slider, rc = react.render_fixed(Test())\n", - "state = rc.state_get()\n", - "box = widgets.VBox()\n", - "hbox, rc = react.render(Test(), box, initial_state=state, handle_error=False)\n", - "slider" + "The following cells displays ipywidgets an solara example of monitoring state changes. Interact with the following widgets to intercept its state changes." ] }, { @@ -107,7 +74,7 @@ "outputs": [], "source": [ "int_value = solara.reactive(0)\n", - "solara.SliderInt(\"Another Test Slider:\", value=int_value, min=0, max=10)" + "slider = solara.SliderInt(\"Another Test Slider:\", value=int_value, min=0, max=10)" ] } ], diff --git a/nbs/02_widget_history.ipynb b/nbs/02_widget_history.ipynb new file mode 100644 index 0000000..a9b6265 --- /dev/null +++ b/nbs/02_widget_history.ipynb @@ -0,0 +1,119 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "c6bc2197-18a4-4b3c-9124-9e8eb3eccbdb", + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp history" + ] + }, + { + "cell_type": "markdown", + "id": "06df2b84-4f6b-4933-bedb-971fea8d447e", + "metadata": {}, + "source": [ + "# Widget history" + ] + }, + { + "cell_type": "markdown", + "id": "fa1e8874-8cfa-4d6c-8af6-75e7f4b71141", + "metadata": {}, + "source": [ + "Store a fixed number of stored states in-memory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c43c72ad-2594-4fa7-a905-b14db392c09e", + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "from collections import deque\n", + "from typing import Any, Deque, Dict, Iterator" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ae746d4-240e-4ed7-9681-76b7a72af0bf", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class WidgetStateHistory:\n", + " def __init__(self, history_size: int = 5):\n", + " self.history_size = history_size\n", + " self._widget_states: Dict[str, Any] = {}\n", + "\n", + " def set_state(self, comm_id: str, state: Any):\n", + " if comm_id not in self._widget_states:\n", + " self._widget_states[comm_id] = deque(maxlen=self.history_size)\n", + "\n", + " self._widget_states[comm_id].append(state)\n", + "\n", + " def get_current_state(self, comm_id: str):\n", + " if comm_id in self._widget_states and self._widget_states[comm_id]:\n", + " return self._widget_states[comm_id][-1]\n", + "\n", + " raise KeyError(f\"No state found for widget comm_id: {comm_id}\")\n", + "\n", + " def get_state_history(self, comm_id: str) -> Any:\n", + " if comm_id in self._widget_states:\n", + " return self._widget_states[comm_id]\n", + "\n", + " raise KeyError(f\"No history found for widget comm_id: {comm_id}\")\n", + "\n", + " def __setitem__(self, comm_id: str, state: Any):\n", + " self.set_state(comm_id, state)\n", + "\n", + " def __getitem__(self, comm_id: str):\n", + " return self.get_current_state(comm_id)\n", + "\n", + " def __delitem__(self, comm_id: str):\n", + " if comm_id in self._widget_states:\n", + " del self._widget_states[comm_id]\n", + " else:\n", + " raise KeyError(f\"Comm ID {comm_id} not found\")\n", + "\n", + " def __iter__(self) -> Iterator[str]:\n", + " return iter(self._widget_states)\n", + "\n", + " def __len__(self) -> int:\n", + " return len(self._widget_states)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "323932aa-7412-4c70-9a1f-a3b313ad5139", + "metadata": {}, + "outputs": [], + "source": [ + "widget_states = WidgetStateHistory(history_size=5)\n", + "\n", + "widget_states[\"widget_1\"] = {\"value\": 10}\n", + "widget_states[\"widget_1\"] = {\"value\": 20}\n", + "assert len(widget_states) == 1\n", + "assert widget_states['widget_1'] == {\"value\": 20}\n", + "\n", + "del widget_states[\"widget_1\"] \n", + "assert len(widget_states) == 0" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbs/03_ipywatch.ipynb b/nbs/03_ipywatch.ipynb new file mode 100644 index 0000000..d857a13 --- /dev/null +++ b/nbs/03_ipywatch.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "e8dcd736-dc1a-41b1-868e-ba87c175c549", + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp ipywatch" + ] + }, + { + "cell_type": "markdown", + "id": "50f735ae-a779-49e5-bb15-6236acdc9b36", + "metadata": {}, + "source": [ + "# Ipywatch" + ] + }, + { + "cell_type": "markdown", + "id": "e11fde7d-c213-44be-b41e-04fe6961b260", + "metadata": {}, + "source": [ + "Widget to watch comm events" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faaa1a37-6353-4563-960d-f74f778eacb8", + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "from typing import Any, Callable, List\n", + "\n", + "import ipywidgets\n", + "from ipykernel.comm import Comm\n", + "from ipywidgets import HBox, VBox, Label, Output, Button, Text, Tab, Layout\n", + "\n", + "from ipywatch.history import WidgetStateHistory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de0af391-46d0-44a3-9554-64845997509c", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class WidgetStateHistoryListener:\n", + " def __init__(\n", + " self, \n", + " history_size: int = 5, \n", + " on_state_change: Callable[[ipywidgets.Widget, Any], None]=None\n", + " ):\n", + " self.history_size = history_size\n", + " self.history = WidgetStateHistory(history_size)\n", + " self.on_state_change = on_state_change\n", + "\n", + " _original_send = Comm.send\n", + "\n", + " def _patched_send(comm, data=None, metadata=None, buffers=None):\n", + " comm_id = comm.comm_id\n", + "\n", + " widget = ipywidgets.Widget.widgets.get(comm_id)\n", + "\n", + " self.history.set_state(comm_id, data)\n", + "\n", + " if self.on_state_change:\n", + " self.on_state_change(widget, data)\n", + "\n", + " _original_send(comm, data, metadata, buffers)\n", + "\n", + " Comm.send = _patched_send" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a0b40b1-d84c-4489-8819-94d6497b5855", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class Ipywatch(HBox):\n", + " def __init__(self, width: str = '100%', height: str = '400px', history_size: int = 5, **kwargs):\n", + " self.updating = False # Flag to prevent recursion\n", + " \n", + " self.listener = WidgetStateHistoryListener(history_size=history_size)\n", + "\n", + " self.messages = Output(layout=dict(width='100%', height='400px'))\n", + "\n", + " self.listener.on_state_change = self._on_state_change\n", + "\n", + " super().__init__(\n", + " children=[self.messages],\n", + " layout=Layout(width=width, height=height)\n", + " )\n", + "\n", + " def _on_state_change(self, widget, state):\n", + " if self.updating:\n", + " return\n", + "\n", + " self.updating = True\n", + "\n", + " widget_type = type(widget).__name__ if widget else \"Unknown\"\n", + " self.messages.append_stdout(f\"Event emitted by {widget_type}: {state}\\n\")\n", + "\n", + " self.updating = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdcc01b0-ddb6-45a0-b170-4053b4317a13", + "metadata": {}, + "outputs": [], + "source": [ + "ipywatch = Ipywatch(width='100%', height='200px')\n", + "slider = ipywidgets.IntSlider(value=7, min=0, max=10, step=1, description='Test Slider:')\n", + "HBox([slider, ipywatch])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7567286-4e4a-4f77-ae5f-00f91d36fc88", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/settings.ini b/settings.ini index 3b04375..164e272 100644 --- a/settings.ini +++ b/settings.ini @@ -3,16 +3,16 @@ # See https://github.com/fastai/nbdev/blob/master/settings.ini for examples. ### Python library ### -repo = debugbar +repo = ipywatch lib_name = %(repo)s version = 0.0.1 -min_python = 3.7 +min_python = 3.9 license = apache2 black_formatting = False ### nbdev ### doc_path = _docs -lib_path = debugbar +lib_path = ipywatch nbs_path = nbs recursive = True tst_flags = notest @@ -38,6 +38,6 @@ status = 3 user = itepifanio ### Optional ### -requirements = solara>=1.26.1 +requirements = solara>=1.26.1 # dev_requirements = # console_scripts = \ No newline at end of file