Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch the full widget state via a control Comm #3021

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the tests would be inspecting the messages a little closer, e.g. check the buffer decoding etc.

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):
martinRenou marked this conversation as resolved.
Show resolved Hide resolved
"""
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__))
martinRenou marked this conversation as resolved.
Show resolved Hide resolved

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> ]
}
}
```