diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index a3e51a9..34ca8a0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -507,6 +507,7 @@ class InteractiveShell(SingletonConfigurable): self.init_pdb() self.init_extension_manager() self.init_payload() + self.init_widgets() self.hooks.late_startup_hook() atexit.register(self.atexit_operations) @@ -2317,7 +2318,15 @@ class InteractiveShell(SingletonConfigurable): def init_payload(self): self.payload_manager = PayloadManager(parent=self) self.configurables.append(self.payload_manager) - + + #------------------------------------------------------------------------- + # Things related to widgets + #------------------------------------------------------------------------- + + def init_widgets(self): + # not implemented in the base class + pass + #------------------------------------------------------------------------- # Things related to the prefilter #------------------------------------------------------------------------- diff --git a/IPython/kernel/widgets/__init__.py b/IPython/kernel/widgets/__init__.py new file mode 100644 index 0000000..0970d03 --- /dev/null +++ b/IPython/kernel/widgets/__init__.py @@ -0,0 +1,2 @@ +from .manager import * +from .widget import * diff --git a/IPython/kernel/widgets/manager.py b/IPython/kernel/widgets/manager.py new file mode 100644 index 0000000..0a2ca46 --- /dev/null +++ b/IPython/kernel/widgets/manager.py @@ -0,0 +1,115 @@ +"""Base class to manage widgets""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2013 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from weakref import ref + +from IPython.config import LoggingConfigurable +from IPython.core.prompts import LazyEvaluate +from IPython.core.getipython import get_ipython +from IPython.utils.traitlets import Instance, Unicode, Dict, Any + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + +def lazy_keys(dikt): + """Return lazy-evaluated string representation of a dictionary's keys + + Key list is only constructed if it will actually be used. + Used for debug-logging. + """ + return LazyEvaluate(lambda d: list(d.keys())) + +class WidgetManager(LoggingConfigurable): + """Manager for Widgets in the Kernel""" + + shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') + def _shell_default(self): + return get_ipython() + iopub_socket = Any() + def _iopub_socket_default(self): + return self.shell.parent.iopub_socket + session = Instance('IPython.kernel.zmq.session.Session') + def _session_default(self): + if self.shell is None: + return + return self.shell.parent.session + + widgets = Dict() + + # Public APIs + + def register_widget(self, widget): + """Register a new widget""" + self.widgets[widget.widget_id] = ref(widget) + widget.shell = self.shell + widget.iopub_socket = self.iopub_socket + widget.create() + return widget.widget_id + + def unregister_widget(self, widget_id): + """Unregister a widget, and destroy its counterpart""" + # unlike get_widget, this should raise a KeyError + widget_ref = self.widgets.pop(widget_id) + widget = widget_ref() + if widget is None: + # already destroyed, nothing to do + return + widget.destroy() + + def get_widget(self, widget_id): + """Get a widget with a particular id + + Returns the widget if found, otherwise None. + + This will not raise an error, + it will log messages if the widget cannot be found. + """ + if widget_id not in self.widgets: + self.log.error("No such widget: %s", widget_id) + self.log.debug("Current widgets: %s", lazy_keys(self.widgets)) + return + # call, because we store weakrefs + widget = self.widgets[widget_id]() + if widget is None: + self.log.error("Widget %s has been removed", widget_id) + del self.widgets[widget_id] + self.log.debug("Current widgets: %s", lazy_keys(self.widgets)) + return + return widget + + # Message handlers + + def widget_update(self, stream, ident, msg): + """Handler for widget_update messages""" + content = msg['content'] + widget_id = content['widget_id'] + widget = self.get_widget(widget_id) + if widget is None: + # no such widget + return + widget.handle_update(content['data']) + + def widget_destroy(self, stream, ident, msg): + """Handler for widget_destroy messages""" + content = msg['content'] + widget_id = content['widget_id'] + widget = self.get_widget(widget_id) + if widget is None: + # no such widget + return + widget.handle_destroy(content['data']) + del self.widgets[widget_id] + + +__all__ = ['WidgetManager'] diff --git a/IPython/kernel/widgets/widget.py b/IPython/kernel/widgets/widget.py new file mode 100644 index 0000000..f258b69 --- /dev/null +++ b/IPython/kernel/widgets/widget.py @@ -0,0 +1,92 @@ +"""Base class for a Widget""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2013 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import uuid + +from IPython.config import LoggingConfigurable +from IPython.utils.traitlets import Instance, Unicode, Bytes, Bool, Dict, Any + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + +class Widget(LoggingConfigurable): + + shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') + def _shell_default(self): + return get_ipython() + iopub_socket = Any() + def _iopub_socket_default(self): + return self.shell.parent.iopub_socket + session = Instance('IPython.kernel.zmq.session.Session') + def _session_default(self): + if self.shell is None: + return + return self.shell.parent.session + + topic = Bytes() + def _topic_default(self): + return ('widget-%s' % self.widget_id).encode('ascii') + + _destroy_data = Dict(help="data dict, if any, to be included in widget_destroy") + _create_data = Dict(help="data dict, if any, to be included in widget_create") + + _destroyed = Bool(False) + widget_type = Unicode('widget') + widget_id = Unicode() + def _widget_id_default(self): + return uuid.uuid4().hex + + def _publish_msg(self, msg_type, data=None, **keys): + """Helper for sending a widget message on IOPub""" + data = {} if data is None else data + self.session.send(self.iopub_socket, msg_type, + dict(data=data, widget_id=self.widget_id, **keys), + ident=self.topic, + ) + + def __del__(self): + """trigger destroy on gc""" + self.destroy() + + # publishing messages + + def create(self): + """Create the frontend-side version of this widget""" + self._publish_msg('widget_create', self._create_data, widget_type = self.widget_type) + + def destroy(self): + """Destroy the frontend-side version of this widget""" + if self._destroyed: + # only destroy once + return + self._publish_msg('widget_destroy', self._destroy_data) + self._destroyed = True + + def update(self, data=None): + """Update the frontend-side version of this widget""" + self._publish_msg('widget_update', data) + + # handling of incoming messages + + def handle_destroy(self, data): + """Handle a widget_destroy message""" + self.log.debug("handle_destroy %s", data) + + def handle_update(self, data): + """Handle a widget_update message""" + self.log.debug("handle_update %s", data) + self.update_data = data + + +__all__ = ['Widget'] diff --git a/IPython/kernel/zmq/ipkernel.py b/IPython/kernel/zmq/ipkernel.py index ec0d8be..97d121f 100755 --- a/IPython/kernel/zmq/ipkernel.py +++ b/IPython/kernel/zmq/ipkernel.py @@ -168,11 +168,17 @@ class Kernel(Configurable): for msg_type in msg_types: self.shell_handlers[msg_type] = getattr(self, msg_type) + widget_msg_types = [ 'widget_update', 'widget_destroy' ] + widget_manager = self.shell.widget_manager + for msg_type in widget_msg_types: + self.shell_handlers[msg_type] = getattr(widget_manager, msg_type) + control_msg_types = msg_types + [ 'clear_request', 'abort_request' ] self.control_handlers = {} for msg_type in control_msg_types: self.control_handlers[msg_type] = getattr(self, msg_type) + def dispatch_control(self, msg): """dispatch control requests""" idents,msg = self.session.feed_identities(msg, copy=False) diff --git a/IPython/kernel/zmq/zmqshell.py b/IPython/kernel/zmq/zmqshell.py index df7f5a5..fd23421 100644 --- a/IPython/kernel/zmq/zmqshell.py +++ b/IPython/kernel/zmq/zmqshell.py @@ -49,6 +49,7 @@ from IPython.utils.warn import error from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook from IPython.kernel.zmq.datapub import ZMQDataPublisher from IPython.kernel.zmq.session import extract_header +from IPython.kernel.widgets import WidgetManager from session import Session #----------------------------------------------------------------------------- @@ -593,7 +594,10 @@ class ZMQInteractiveShell(InteractiveShell): super(ZMQInteractiveShell, self).init_magics() self.register_magics(KernelMagics) self.magics_manager.register_alias('ed', 'edit') - + + def init_widgets(self): + self.widget_manager = WidgetManager(shell=self, parent=self) + self.configurables.append(self.widget_manager) InteractiveShellABC.register(ZMQInteractiveShell)