Skip to content

Commit 9031259

Browse files
committed
add a use_trait hook attached to widget wrapper
1 parent 506def4 commit 9031259

File tree

7 files changed

+219
-78
lines changed

7 files changed

+219
-78
lines changed

notebooks/introduction.ipynb

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,27 @@
2424
},
2525
{
2626
"cell_type": "code",
27-
"execution_count": null,
27+
"execution_count": 1,
2828
"metadata": {
2929
"tags": []
3030
},
31-
"outputs": [],
31+
"outputs": [
32+
{
33+
"data": {
34+
"application/vnd.jupyter.widget-view+json": {
35+
"model_id": "901546cd31e04580810d8358cbf46d72",
36+
"version_major": 2,
37+
"version_minor": 0
38+
},
39+
"text/plain": [
40+
"LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>)))"
41+
]
42+
},
43+
"execution_count": 1,
44+
"metadata": {},
45+
"output_type": "execute_result"
46+
}
47+
],
3248
"source": [
3349
"from reactpy import component, html\n",
3450
"\n",
@@ -55,11 +71,27 @@
5571
},
5672
{
5773
"cell_type": "code",
58-
"execution_count": null,
74+
"execution_count": 2,
5975
"metadata": {
6076
"tags": []
6177
},
62-
"outputs": [],
78+
"outputs": [
79+
{
80+
"data": {
81+
"application/vnd.jupyter.widget-view+json": {
82+
"model_id": "ee078bce581341f7826d8578cc03f971",
83+
"version_major": 2,
84+
"version_minor": 0
85+
},
86+
"text/plain": [
87+
"LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>)))"
88+
]
89+
},
90+
"execution_count": 2,
91+
"metadata": {},
92+
"output_type": "execute_result"
93+
}
94+
],
6395
"source": [
6496
"from reactpy import component, html\n",
6597
"\n",
@@ -109,11 +141,27 @@
109141
},
110142
{
111143
"cell_type": "code",
112-
"execution_count": null,
144+
"execution_count": 3,
113145
"metadata": {
114146
"tags": []
115147
},
116-
"outputs": [],
148+
"outputs": [
149+
{
150+
"data": {
151+
"application/vnd.jupyter.widget-view+json": {
152+
"model_id": "3e5123eaa6fe49fcb94f2527ec7665e8",
153+
"version_major": 2,
154+
"version_minor": 0
155+
},
156+
"text/plain": [
157+
"LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>)))"
158+
]
159+
},
160+
"execution_count": 3,
161+
"metadata": {},
162+
"output_type": "execute_result"
163+
}
164+
],
117165
"source": [
118166
"import json\n",
119167
"from pathlib import Path\n",
@@ -164,9 +212,25 @@
164212
},
165213
{
166214
"cell_type": "code",
167-
"execution_count": null,
215+
"execution_count": 4,
168216
"metadata": {},
169-
"outputs": [],
217+
"outputs": [
218+
{
219+
"data": {
220+
"application/vnd.jupyter.widget-view+json": {
221+
"model_id": "cb6e7a22534d4db6b1c6b826689cf739",
222+
"version_major": 2,
223+
"version_minor": 0
224+
},
225+
"text/plain": [
226+
"LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>)))"
227+
]
228+
},
229+
"execution_count": 4,
230+
"metadata": {},
231+
"output_type": "execute_result"
232+
}
233+
],
170234
"source": [
171235
"from reactpy_jupyter import from_widget\n",
172236
"from ipywidgets import IntSlider\n",
@@ -182,38 +246,37 @@
182246
"cell_type": "markdown",
183247
"metadata": {},
184248
"source": [
185-
"Let's consider a ReactPy component that responds to and displays changes from an `ipywidgets.IntSlider`. The ReactPy component will need to accept an `IntSlider` instance as one of its arguments, convert it to a component with `from_widget`, declare state that will track the slider's value, and register a lister that will update that state via the slider's `IntSlider.observe()` method using an [\"effect\"](https://reactpy.dev/docs/reference/hooks-api.html#use-effect):"
249+
"Let's consider a ReactPy component that mirrors an `ipywidgets.IntSlider` - that is, it displays a slider that moves when the `IntSlider` does and when moved alters the `IntSlider`. To accomplish this, the ReactPy component will need to accept an `IntSlider` instance as one of its arguments, convert it to a component with `from_widget`, and access the attributes it expects to change or that need to be changed via a `use_trait` method on the converted widget:"
186250
]
187251
},
188252
{
189253
"cell_type": "code",
190-
"execution_count": null,
254+
"execution_count": 10,
191255
"metadata": {
192256
"tags": []
193257
},
194258
"outputs": [],
195259
"source": [
196-
"from reactpy import use_effect\n",
197260
"from reactpy_jupyter import from_widget\n",
198261
"\n",
199262
"\n",
200263
"@component\n",
201-
"def SliderObserver(slider):\n",
202-
" slider_component = from_widget(slider)\n",
203-
" value, set_value = use_state(0)\n",
204-
"\n",
205-
" @use_effect\n",
206-
" def register_observer():\n",
207-
" def handle_change(change):\n",
208-
" set_value(change[\"new\"])\n",
209-
"\n",
210-
" # observe the slider's value\n",
211-
" slider.observe(handle_change, \"value\")\n",
212-
" # unobserve the slider's value if this component is no longer displayed\n",
213-
" return lambda: slider.unobserve(handle_change, \"value\")\n",
214-
"\n",
264+
"def MirrorSlider(slider_widget):\n",
265+
" slider_component = from_widget(slider_widget)\n",
266+
" value, set_value = slider_component.use_trait(\"value\")\n",
215267
" return html.div(\n",
216-
" slider_component, html.p(f\"ReactPy observes the value to be: \", value)\n",
268+
" html.h3(\"Jupyter Slider\"),\n",
269+
" # slider_component,\n",
270+
" html.h3(\"ReactPy Slider\"),\n",
271+
" html.input(\n",
272+
" {\n",
273+
" \"type\": \"range\",\n",
274+
" \"min\": slider_widget.min,\n",
275+
" \"max\": slider_widget.max,\n",
276+
" \"value\": value,\n",
277+
" \"on_change\": lambda event: set_value(event[\"target\"][\"value\"]),\n",
278+
" }\n",
279+
" ),\n",
217280
" )"
218281
]
219282
},
@@ -227,15 +290,31 @@
227290
},
228291
{
229292
"cell_type": "code",
230-
"execution_count": null,
293+
"execution_count": 11,
231294
"metadata": {
232295
"tags": []
233296
},
234-
"outputs": [],
297+
"outputs": [
298+
{
299+
"data": {
300+
"application/vnd.jupyter.widget-view+json": {
301+
"model_id": "48a4b16d9b7149fe9bbc8cbf5c20bd6c",
302+
"version_major": 2,
303+
"version_minor": 0
304+
},
305+
"text/plain": [
306+
"LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>)))"
307+
]
308+
},
309+
"execution_count": 11,
310+
"metadata": {},
311+
"output_type": "execute_result"
312+
}
313+
],
235314
"source": [
236315
"from ipywidgets import IntSlider\n",
237316
"\n",
238-
"SliderObserver(IntSlider(readout=False))"
317+
"MirrorSlider(IntSlider(readout=False))"
239318
]
240319
},
241320
{
@@ -248,19 +327,35 @@
248327
},
249328
{
250329
"cell_type": "code",
251-
"execution_count": null,
330+
"execution_count": 9,
252331
"metadata": {
253332
"tags": []
254333
},
255-
"outputs": [],
334+
"outputs": [
335+
{
336+
"data": {
337+
"application/vnd.jupyter.widget-view+json": {
338+
"model_id": "4fc27c4a9ae04351b140ca4bcb15e5be",
339+
"version_major": 2,
340+
"version_minor": 0
341+
},
342+
"text/plain": [
343+
"Box(children=(LayoutWidget(Layout(ContextProvider(<function context at 0x7fb48b0a3600>))), LayoutWidget(Layout…"
344+
]
345+
},
346+
"execution_count": 9,
347+
"metadata": {},
348+
"output_type": "execute_result"
349+
}
350+
],
256351
"source": [
257352
"from ipywidgets import Box\n",
258353
"from reactpy_jupyter import to_widget\n",
259354
"\n",
260355
"slider = IntSlider(readout=False)\n",
261-
"slider_observer_widget = to_widget(SliderObserver(slider))\n",
356+
"slider_observer_widget = to_widget(MirrorSlider(slider))\n",
262357
"\n",
263-
"Box([slider, slider_observer_widget])"
358+
"Box([slider_observer_widget, slider_observer_widget])"
264359
]
265360
},
266361
{

reactpy_jupyter/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@
55
# Distributed under the terms of the Modified BSD License.
66

77
from . import jupyter_server_extension
8+
from .component_widget import run, set_import_source_base_url, to_widget
9+
from .hooks import use_trait
810
from .import_resources import setup_import_resources
9-
from .layout_widget import run, set_import_source_base_url, to_widget
1011
from .monkey_patch import execute_patch
1112
from .widget_component import from_widget
1213

1314
__version__ = "0.9.5" # DO NOT MODIFY
1415

1516
__all__ = (
1617
"from_widget",
18+
"jupyter_server_extension",
1719
"load_ipython_extension",
18-
"unload_ipython_extension",
19-
"to_widget",
2020
"run",
2121
"set_import_source_base_url",
22-
"jupyter_server_extension",
22+
"to_widget",
23+
"unload_ipython_extension",
24+
"use_trait",
2325
)
2426

2527

reactpy_jupyter/layout_widget.py renamed to reactpy_jupyter/component_widget.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,38 +35,38 @@ def run(constructor: Callable[[], ComponentType]) -> DisplayHandle | None:
3535
3636
This function is meant to be similarly to ``reactpy.run``.
3737
"""
38-
return ipython_display(LayoutWidget(constructor()))
38+
return ipython_display(ComponentWidget(constructor()))
3939

4040

4141
_P = ParamSpec("_P")
4242

4343

4444
@overload
45-
def to_widget(value: Callable[_P, ComponentType]) -> Callable[_P, LayoutWidget]:
45+
def to_widget(value: Callable[_P, ComponentType]) -> Callable[_P, ComponentWidget]:
4646
...
4747

4848

4949
@overload
50-
def to_widget(value: ComponentType) -> LayoutWidget:
50+
def to_widget(value: ComponentType) -> ComponentWidget:
5151
...
5252

5353

5454
def to_widget(
5555
value: Callable[_P, ComponentType] | ComponentType
56-
) -> Callable[_P, LayoutWidget] | LayoutWidget:
56+
) -> Callable[_P, ComponentWidget] | ComponentWidget:
5757
"""Turn a component into a widget or a component construtor into a widget constructor"""
5858

5959
if isinstance(value, ComponentType):
60-
return LayoutWidget(value)
60+
return ComponentWidget(value)
6161

6262
@wraps(value)
63-
def wrapper(*args: Any, **kwargs: Any) -> LayoutWidget:
64-
return LayoutWidget(value(*args, **kwargs))
63+
def wrapper(*args: Any, **kwargs: Any) -> ComponentWidget:
64+
return ComponentWidget(value(*args, **kwargs))
6565

6666
return wrapper
6767

6868

69-
class LayoutWidget(anywidget.AnyWidget):
69+
class ComponentWidget(anywidget.AnyWidget):
7070
"""A widget for displaying ReactPy elements"""
7171

7272
_esm = ESM

reactpy_jupyter/hooks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import Any
2+
3+
from reactpy import use_effect, use_state
4+
from reactpy.types import State
5+
from traitlets import HasTraits
6+
7+
8+
def use_trait(obj: HasTraits, name: str) -> State[Any]:
9+
"""Hook to use the attribute of a HasTraits object as a state variable
10+
11+
This works on Jupyter Widgets, for example.
12+
"""
13+
value, set_value = use_state(lambda: getattr(obj, name))
14+
15+
@use_effect
16+
def register_observer():
17+
def handle_change(change):
18+
set_value(change["new"])
19+
20+
# observe the slider's value
21+
obj.observe(handle_change, "value")
22+
# unobserve the slider's value if this component is no longer displayed
23+
return lambda: obj.unobserve(handle_change, "value")
24+
25+
def set_trait(new_value: Any) -> None:
26+
setattr(obj, name, new_value)
27+
28+
return State(value, set_trait)

reactpy_jupyter/import_resources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import requests
1111
from notebook import notebookapp
1212

13+
from .component_widget import set_import_source_base_url
1314
from .jupyter_server_extension import (
1415
REACTPY_RESOURCE_BASE_PATH,
1516
REACTPY_WEB_MODULES_DIR,
1617
)
17-
from .layout_widget import set_import_source_base_url
1818

1919
logger = logging.getLogger(__name__)
2020

reactpy_jupyter/monkey_patch.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
1-
from typing import Any
2-
from weakref import finalize
3-
41
from reactpy.core.component import Component
52

6-
from reactpy_jupyter.layout_widget import to_widget
7-
8-
# we can't track the widgets by adding them as a hidden attribute to the component
9-
# because Component has __slots__ defined
10-
LIVE_WIDGETS: dict[int, Any] = {}
3+
from reactpy_jupyter.widget_component import WidgetComponent
114

125

136
def execute_patch() -> None:
147
"""Monkey patch ReactPy's Component class to display as a Jupyter widget"""
15-
16-
def _repr_mimebundle_(self: Component, *a, **kw) -> None:
17-
self_id = id(self)
18-
if self_id not in LIVE_WIDGETS:
19-
widget = LIVE_WIDGETS[self_id] = to_widget(self)
20-
finalize(self, lambda: LIVE_WIDGETS.pop(self_id, None))
21-
else:
22-
widget = LIVE_WIDGETS[self_id]
23-
return widget._repr_mimebundle_(*a, **kw)
24-
25-
Component._repr_mimebundle_ = _repr_mimebundle_
8+
Component._repr_mimebundle_ = WidgetComponent._repr_mimebundle_

0 commit comments

Comments
 (0)