-
Notifications
You must be signed in to change notification settings - Fork 0
IPEP 21: Widget Messages
Status | Active |
Author | Min RK <benjaminrk@gmail.com> |
Created | September 11, 2013 |
Updated | September 12, 2013 |
Adding communication for use in interactive widgets.
IPython has a message specification for the Frontend (Javascript, etc.) to communicate with the Kernel and vice versa. We want developers to be able to build their own interactive tools on top of IPython by providing an API for creating a pair of objects - one Kernel-side, one Frontend-side - and facilitating their communication, and a messaging specification, so that these can be implemented in kernels other than the one provided in IPython.
This proposal includes message types for dispatching messages to particular communicators, each of which has a unique ID. This should allow developers to set up communication between their own Kernel and Frontend-side objects.
In general, these messages are one-way with no reply.
Kernels send messages to frontends on IOPub, and frontends send messages to Kernels on Shell.
All new messages have a data
key, which is any JSONable dictionary.
Essentially, this dictionary allows any widget developer to define their own message specification
for their objects to communicate with each other, without risk of message type or API collision.
Creating a communication pipe can be done on either side,
and doing so produces produces a comm_open
message.
{
'comm_id' : 'u-u-i-d',
'target' : 'some_key',
'data' : {}
}
When one of these messages is received, the recipient should create a new Comm with matching comm_id
.
The target
key is used to trigger some event when the Comm is created,
such as instantiating a peer Widget.
If comm creation fails, a comm_destroy
message should be sent,
because no Comm should exist without its peer.
Every Comm has a unique ID, shared only with its counterpart.
The data
key can be any extra JSON information used in initialization of the widget.
This is the primary method for peers to communicate. Comm messages are one-way communications, which can be used to update widget state, either on the Kernel or the frontend side. Both sides can send these messages.
The kernel sends these messages on the IOPub channel, and the frontend sends them on the Shell channel.
There are no expected replies (of course, one side can send another widget_msg
in reply).
Message type: comm_msg
:
{
'comm_id' : 'u-u-i-d',
'data' : {}
}
Since Comms live on both sides, when one comm is closed the other side must be notified.
This is done with a comm_close
message,
which can come from either side via IOPub or Shell.
Message type: comm_close
:
{
'comm_id' : 'u-u-i-d',
'data' : {}
}
This includes specification for the API of the Python and Javascript implementations of this message specification. It is recommended that any Kernel implementation follow suit as appropriate for the language, but only the message specification is necessary.
Both the Javascript and Python sides have two basic objects:
- Comm
- CommManager
There will be a base class for Comms on both sides.
Each Comm instance has a comm_id
, and a counterpart on the other side.
When an object creates a Comm, it should pass a target
argument,
a string key used to identify the handler for the other side.
Each widget has four primary methods:
-
open(data?)
, which sends acomm_open
message -
send(data)
, which sends awidget_msg
message -
close(data?)
, which sends awidget_close
message -
handle_open(data)
, called when acomm_open
message arrives, after -
handle_msg(data)
, called when awidget_msg
message arrives -
handle_close(data)
, called when awidget_close
message arrives
This is the basic encapsulation of widget communication.
Question: the handle_foo messages get the data
dict, not the full message.
This means they don't have access to metadata, etc. Should the handlers get the full message instead?
Another Question: Should open/close get a data
method? When I have implemented these,
open and close are called by __init__
and __del__
, which means there is no easy mechanism
to pass arguments to open and close (other than private attributes).
The CommManager maintains a mapping of comm_id to Comm instance. To add a widget to the mapping, use:
comm_id = CommManager.register_widget(widget)
CommManagers also maintain a mapping of target
to callbacks,
for handling the creation of a new comm.
Typically, these would be constructors for Widget objects.
The callback will be passed only one argument: the connected Comm instance.
Comm.handle_open()
will be called after the handler,
so event listeners for on_open registered by the callback will be called.
To register a callback with a target key, use:
CommManager.register_target('target', MyCommClass)
This mechanism determines what objects can be created from the other side, and functions as a whitelist.
For instance, a Javascript plugin that defines a new widget will need to call this before any widgets can be instantiated:
IPython.widget_manager.register_target('mywidget', my_js_func);
The IPython Kernel equivalent is
get_ipython().widget_manager.register_target('mywidget', MyPythonWidget)
The CommManager has a method for each message type registered as a handler
(CommManager.widget_msg(msg)
, etc.).
The Kernel-side manager registers them on the Shell channel,
and the Frontend manager registers them on the IOPub channel.
Any time a comm
message arrives, it is dispatched to the appropriate Comm instance
in the mapping, identified by the comm_id
key in the message content.
The Comm's handle_foo
method is then called with the data
dict from the message contents.
In IPython, instantiating a Comm automatically registers the instance with the CommManager,
and sends the comm_open
message on the IOPub channel.
So as soon as you have a Comm instance, it is ready to use.
Since we do not have a mechanism for storing this kind of state in the notebook document,
reloading the page will close the Javascript-side instances of all comms,
but the Kernel-side instances will all remain without their peers.
Restarting the Kernel does the same, in reverse.
Re-sending comm_open
messages for all extant Comms should re-establish connections,
so perhaps there should be an API call on either side that does this.