Skip to content

Commit

Permalink
Merge pull request #3021 from maartenbreddels/refactor_widget_fetching
Browse files Browse the repository at this point in the history
Fetch the full widget state via a control Comm
  • Loading branch information
vidartf authored Nov 16, 2021
2 parents 77ebf6d + 430013e commit 16ffbc9
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 4 deletions.
1 change: 1 addition & 0 deletions ipywidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def register_comm_target(kernel=None):
if kernel is None:
kernel = get_ipython().kernel
kernel.comm_manager.register_target('jupyter.widget', Widget.handle_comm_opened)
kernel.comm_manager.register_target('jupyter.widget.control', Widget.handle_control_comm_opened)

# deprecated alias
handle_kernel = register_comm_target
Expand Down
1 change: 1 addition & 0 deletions ipywidgets/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4]))

__protocol_version__ = '2.0.0'
__control_protocol_version__ = '1.0.0'

# 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.
__jupyter_widgets_base_version__ = '1.2.0'
Expand Down
11 changes: 10 additions & 1 deletion ipywidgets/widgets/tests/test_send_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from traitlets import Bool, Tuple, List

from .utils import setup, teardown
from .utils import setup, teardown, DummyComm

from ..widget import Widget

Expand All @@ -23,3 +23,12 @@ def test_empty_hold_sync():
with w.hold_sync():
pass
assert w.comm.messages == []


def test_control():
comm = DummyComm()
Widget.close_all()
w = SimpleWidget()
Widget.handle_control_comm_opened(comm, {})
Widget.handle_control_comm_msg({'type': 'models-request'})
assert comm.messages
50 changes: 48 additions & 2 deletions ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@

from base64 import standard_b64encode

from .._version import __protocol_version__, __jupyter_widgets_base_version__
from .._version import __protocol_version__, __control_protocol_version__, __jupyter_widgets_base_version__
PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0]
CONTROL_PROTOCOL_VERSION_MAJOR = __control_protocol_version__.split('.')[0]

def _widget_to_json(x, obj):
if isinstance(x, dict):
Expand Down Expand Up @@ -290,6 +291,7 @@ class Widget(LoggingHasTraits):
# Class attributes
#-------------------------------------------------------------------------
_widget_construction_callback = None
_control_comm = None

# widgets is a dictionary of all active widget objects
widgets = {}
Expand All @@ -302,7 +304,6 @@ def close_all(cls):
for widget in list(cls.widgets.values()):
widget.close()


@staticmethod
def on_widget_constructed(callback):
"""Registers a callback to be called when a widget is constructed.
Expand All @@ -317,6 +318,51 @@ def _call_widget_constructed(widget):
if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
Widget._widget_construction_callback(widget)

@classmethod
def handle_control_comm_opened(cls, comm, msg):
"""
Class method, called when the comm-open message on the
"jupyter.widget.control" comm channel is received
"""
version = msg.get('metadata', {}).get('version', '')
if version.split('.')[0] != CONTROL_PROTOCOL_VERSION_MAJOR:
raise ValueError("Incompatible widget control protocol versions: received version %r, expected version %r"%(version, __control_protocol_version__))

cls._control_comm = comm
cls._control_comm.on_msg(cls._handle_control_comm_msg)

@classmethod
def _handle_control_comm_msg(cls, msg):
# This shouldn't happen unless someone calls this method manually
if cls._control_comm is None:
raise RuntimeError('Control comm has not been properly opened')

data = msg['content']['data']
method = data['method']

if method == 'request_states':
# Send back the full widgets state
cls.get_manager_state()
widgets = cls.widgets.values()
full_state = {}
drop_defaults = False
for widget in widgets:
full_state[widget.model_id] = {
'model_name': widget._model_name,
'model_module': widget._model_module,
'model_module_version': widget._model_module_version,
'state': widget.get_state(drop_defaults=drop_defaults),
}
full_state, buffer_paths, buffers = _remove_buffers(full_state)
cls._control_comm.send(dict(
method='update_states',
states=full_state,
buffer_paths=buffer_paths
), buffers=buffers)

else:
self.log.error('Unknown front-end to back-end widget control msg with method "%s"' % method)

@staticmethod
def handle_comm_opened(comm, msg):
"""Static method, called when a widget is constructed."""
Expand Down
1 change: 0 additions & 1 deletion packages/base/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ const JUPYTER_WIDGETS_VERSION = '1.2.0';

export
const PROTOCOL_VERSION = '2.0.0';

41 changes: 41 additions & 0 deletions packages/schema/messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,44 @@ To display a widget, the kernel sends a Jupyter [iopub `display_data` message](h
}
}
```




# Control Widget messaging protocol, version 1.0

This is implemented in ipywidgets 7.7.

### The `jupyter.widget.control` comm target

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,

#### State requests: `request_states`

When a frontend wants to request the full state of a all widgets, the frontend sends a `request_states` message:

```
{
'comm_id' : 'u-u-i-d',
'data' : {
'method': 'request_states'
}
}
```

The kernel side of the widget should immediately send an `update_states` message with all widgets states:

```
{
'comm_id' : 'u-u-i-d',
'data' : {
'method': 'update_states',
'states': {
<widget1 u-u-i-d>: <widget1 state>,
<widget2 u-u-i-d>: <widget2 state>,
[...]
},
'buffer_paths': [ <list with paths corresponding to the binary buffers> ]
}
}
```

0 comments on commit 16ffbc9

Please sign in to comment.