Skip to content

Commit 584fb26

Browse files
authored
Merge pull request #3313 from martinRenou/control_channel
Implement jupyter.widget.control comm channel
2 parents 4160af3 + b0e7ecf commit 584fb26

File tree

5 files changed

+107
-3
lines changed

5 files changed

+107
-3
lines changed

packages/schema/messages.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,44 @@ To display a widget, the kernel sends a Jupyter [iopub `display_data` message](h
338338
}
339339
}
340340
```
341+
342+
343+
344+
345+
# Control Widget messaging protocol, version 1.0
346+
347+
This is implemented in ipywidgets 7.7.
348+
349+
### The `jupyter.widget.control` comm target
350+
351+
A kernel-side Jupyter widgets library registers a `jupyter.widget.control` comm target that is used for fetching all widgets states through a "one shot" comm message (one for all widget instances). Unlike the `jupyter.widget` comm target, the created comm is global to all widgets,
352+
353+
#### State requests: `request_states`
354+
355+
When a frontend wants to request the full state of a all widgets, the frontend sends a `request_states` message:
356+
357+
```
358+
{
359+
'comm_id' : 'u-u-i-d',
360+
'data' : {
361+
'method': 'request_states'
362+
}
363+
}
364+
```
365+
366+
The kernel side of the widget should immediately send an `update_states` message with all widgets states:
367+
368+
```
369+
{
370+
'comm_id' : 'u-u-i-d',
371+
'data' : {
372+
'method': 'update_states',
373+
'states': {
374+
<widget1 u-u-i-d>: <widget1 state>,
375+
<widget2 u-u-i-d>: <widget2 state>,
376+
[...]
377+
},
378+
'buffer_paths': [ <list with paths corresponding to the binary buffers> ]
379+
}
380+
}
381+
```

python/ipywidgets/ipywidgets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def register_comm_target(kernel=None):
3636
if kernel is None:
3737
kernel = get_ipython().kernel
3838
kernel.comm_manager.register_target('jupyter.widget', Widget.handle_comm_opened)
39+
kernel.comm_manager.register_target('jupyter.widget.control', Widget.handle_control_comm_opened)
3940

4041
def _handle_ipython():
4142
"""Register with the comm target at import if running in Jupyter"""

python/ipywidgets/ipywidgets/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
__version__ = '8.0.0b0'
55

66
__protocol_version__ = '2.0.0'
7+
__control_protocol_version__ = '1.0.0'
78

89
# These are *protocol* versions for each package, *not* npm versions. To check, look at each package's src/version.ts file for the protocol version the package implements.
910
__jupyter_widgets_base_version__ = '2.0.0'

python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
from traitlets import Bool, Tuple, List
55

6-
from .utils import setup, teardown
6+
from .utils import setup, teardown, DummyComm
77

88
from ..widget import Widget
99

10+
from ..._version import __control_protocol_version__
11+
1012
# A widget with simple traits
1113
class SimpleWidget(Widget):
1214
a = Bool().tag(sync=True)
@@ -23,3 +25,16 @@ def test_empty_hold_sync():
2325
with w.hold_sync():
2426
pass
2527
assert w.comm.messages == []
28+
29+
30+
def test_control():
31+
comm = DummyComm()
32+
Widget.close_all()
33+
w = SimpleWidget()
34+
Widget.handle_control_comm_opened(
35+
comm, dict(metadata={'version': __control_protocol_version__})
36+
)
37+
Widget._handle_control_comm_msg(dict(content=dict(
38+
data={'method': 'request_states'}
39+
)))
40+
assert comm.messages

python/ipywidgets/ipywidgets/widgets/widget.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
from base64 import standard_b64encode
1919

20-
from .._version import __protocol_version__, __jupyter_widgets_base_version__
20+
from .._version import __protocol_version__, __control_protocol_version__, __jupyter_widgets_base_version__
2121
PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0]
22+
CONTROL_PROTOCOL_VERSION_MAJOR = __control_protocol_version__.split('.')[0]
2223

2324
def _widget_to_json(x, obj):
2425
if isinstance(x, dict):
@@ -262,6 +263,7 @@ class Widget(LoggingHasTraits):
262263
# Class attributes
263264
#-------------------------------------------------------------------------
264265
_widget_construction_callback = None
266+
_control_comm = None
265267

266268
# _active_widgets is a dictionary of all active widget objects
267269
_active_widgets = {}
@@ -274,7 +276,6 @@ def close_all(cls):
274276
for widget in list(cls._active_widgets.values()):
275277
widget.close()
276278

277-
278279
@staticmethod
279280
def on_widget_constructed(callback):
280281
"""Registers a callback to be called when a widget is constructed.
@@ -289,6 +290,51 @@ def _call_widget_constructed(widget):
289290
if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
290291
Widget._widget_construction_callback(widget)
291292

293+
@classmethod
294+
def handle_control_comm_opened(cls, comm, msg):
295+
"""
296+
Class method, called when the comm-open message on the
297+
"jupyter.widget.control" comm channel is received
298+
"""
299+
version = msg.get('metadata', {}).get('version', '')
300+
if version.split('.')[0] != CONTROL_PROTOCOL_VERSION_MAJOR:
301+
raise ValueError("Incompatible widget control protocol versions: received version %r, expected version %r"%(version, __control_protocol_version__))
302+
303+
cls._control_comm = comm
304+
cls._control_comm.on_msg(cls._handle_control_comm_msg)
305+
306+
@classmethod
307+
def _handle_control_comm_msg(cls, msg):
308+
# This shouldn't happen unless someone calls this method manually
309+
if cls._control_comm is None:
310+
raise RuntimeError('Control comm has not been properly opened')
311+
312+
data = msg['content']['data']
313+
method = data['method']
314+
315+
if method == 'request_states':
316+
# Send back the full widgets state
317+
cls.get_manager_state()
318+
widgets = cls._active_widgets.values()
319+
full_state = {}
320+
drop_defaults = False
321+
for widget in widgets:
322+
full_state[widget.model_id] = {
323+
'model_name': widget._model_name,
324+
'model_module': widget._model_module,
325+
'model_module_version': widget._model_module_version,
326+
'state': widget.get_state(drop_defaults=drop_defaults),
327+
}
328+
full_state, buffer_paths, buffers = _remove_buffers(full_state)
329+
cls._control_comm.send(dict(
330+
method='update_states',
331+
states=full_state,
332+
buffer_paths=buffer_paths
333+
), buffers=buffers)
334+
335+
else:
336+
raise RuntimeError('Unknown front-end to back-end widget control msg with method "%s"' % method)
337+
292338
@staticmethod
293339
def handle_comm_opened(comm, msg):
294340
"""Static method, called when a widget is constructed."""

0 commit comments

Comments
 (0)