diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index 22f0cd506..6a8830992 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -18,14 +18,79 @@ import types
from IPython.kernel.comm import Comm
from IPython.config import LoggingConfigurable
-from IPython.utils.traitlets import Unicode, Dict, Instance, Bool
+from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List
from IPython.utils.py3compat import string_types
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
-class Widget(LoggingConfigurable):
+class CallbackDispatcher(LoggingConfigurable):
+ acceptable_nargs = List([], help="""List of integers.
+ The number of arguments in the callbacks registered must match one of
+ the integers in this list. If this list is empty or None, it will be
+ ignored.""")
+
+ def __init__(self, *pargs, **kwargs):
+ """Constructor"""
+ LoggingConfigurable.__init__(self, *pargs, **kwargs)
+ self.callbacks = {}
+
+ def __call__(self, *pargs, **kwargs):
+ """Call all of the registered callbacks that have the same number of
+ positional arguments."""
+ nargs = len(pargs)
+ self._validate_nargs(nargs)
+ if nargs in self.callbacks:
+ for callback in self.callbacks[nargs]:
+ callback(*pargs, **kwargs)
+
+ def register_callback(self, callback, remove=False):
+ """(Un)Register a callback
+
+ Parameters
+ ----------
+ callback: method handle
+ Method to be registered or unregisted.
+ remove=False: bool
+ Whether or not to unregister the callback."""
+
+ # Validate the number of arguments that the callback accepts.
+ nargs = self._get_nargs(callback)
+ self._validate_nargs(nargs)
+
+ # Get/create the appropriate list of callbacks.
+ if nargs not in self.callbacks:
+ self.callbacks[nargs] = []
+ callback_list = self.callbacks[nargs]
+
+ # (Un)Register the callback.
+ if remove and callback in callback_list:
+ callback_list.remove(callback)
+ else not remove and callback not in callback_list:
+ callback_list.append(callback)
+
+ def _validate_nargs(self, nargs):
+ if self.acceptable_nargs is not None and \
+ len(self.acceptable_nargs) > 0 and \
+ nargs not in self.acceptable_nargs:
+
+ raise TypeError('Invalid number of positional arguments. See acceptable_nargs list.')
+
+ def _get_nargs(self, callback):
+ """Gets the number of arguments in a callback"""
+ if callable(callback):
+ argspec = inspect.getargspec(callback)
+ nargs = len(argspec[1]) # Only count vargs!
+
+ # Bound methods have an additional 'self' argument
+ if isinstance(callback, types.MethodType):
+ nargs -= 1
+ return nargs
+ else:
+ raise TypeError('Callback must be callable.')
+
+class Widget(LoggingConfigurable):
#-------------------------------------------------------------------------
# Class attributes
#-------------------------------------------------------------------------
@@ -59,10 +124,13 @@ class Widget(LoggingConfigurable):
def __init__(self, **kwargs):
"""Public constructor"""
self.closed = False
+
self._property_lock = (None, None)
- self._display_callbacks = []
- self._msg_callbacks = []
self._keys = None
+
+ self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
+ self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
+
super(Widget, self).__init__(**kwargs)
self.on_trait_change(self._handle_property_changed, self.keys)
@@ -162,31 +230,11 @@ class Widget(LoggingConfigurable):
----------
callback: method handler
Can have a signature of:
- - callback(content)
- - callback(sender, content)
+ - callback(content) Signature 1
+ - callback(sender, content) Signature 2
remove: bool
True if the callback should be unregistered."""
- if remove and callback in self._msg_callbacks:
- self._msg_callbacks.remove(callback)
- elif not remove and not callback in self._msg_callbacks:
- if callable(callback):
- argspec = inspect.getargspec(callback)
- nargs = len(argspec[0])
-
- # Bound methods have an additional 'self' argument
- if isinstance(callback, types.MethodType):
- nargs -= 1
-
- # Call the callback
- if nargs == 1:
- self._msg_callbacks.append(lambda sender, content: callback(content))
- elif nargs == 2:
- self._msg_callbacks.append(callback)
- else:
- raise TypeError('Widget msg callback must ' \
- 'accept 1 or 2 arguments, not %d.' % nargs)
- else:
- raise Exception('Callback must be callable.')
+ self._msg_callbacks.register_callback(callback, remove=remove)
def on_displayed(self, callback, remove=False):
"""(Un)Register a widget displayed callback.
@@ -199,13 +247,7 @@ class Widget(LoggingConfigurable):
kwargs from display call passed through without modification.
remove: bool
True if the callback should be unregistered."""
- if remove and callback in self._display_callbacks:
- self._display_callbacks.remove(callback)
- elif not remove and not callback in self._display_callbacks:
- if callable(handler):
- self._display_callbacks.append(callback)
- else:
- raise Exception('Callback must be callable.')
+ self._display_callbacks.register_callback(callback, remove=remove)
#-------------------------------------------------------------------------
# Support methods
@@ -262,8 +304,8 @@ class Widget(LoggingConfigurable):
def _handle_custom_msg(self, content):
"""Called when a custom msg is received."""
- for handler in self._msg_callbacks:
- handler(self, content)
+ self._msg_callbacks(content) # Signature 1
+ self._msg_callbacks(self, content) # Signature 2
def _handle_property_changed(self, name, old, new):
"""Called when a property has been changed."""
@@ -274,8 +316,7 @@ class Widget(LoggingConfigurable):
def _handle_displayed(self, **kwargs):
"""Called when a view has been displayed for this widget instance"""
- for handler in self._display_callbacks:
- handler(self, **kwargs)
+ self._display_callbacks(**kwargs)
def _pack_widgets(self, x):
"""Recursively converts all widget instances to model id strings.
diff --git a/IPython/html/widgets/widget_button.py b/IPython/html/widgets/widget_button.py
index 326b36793..cedc1f84d 100644
--- a/IPython/html/widgets/widget_button.py
+++ b/IPython/html/widgets/widget_button.py
@@ -14,10 +14,7 @@ click events on the button and trigger backend code when the clicks are fired.
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
-import inspect
-import types
-
-from .widget import DOMWidget
+from .widget import DOMWidget, CallbackDispatcher
from IPython.utils.traitlets import Unicode, Bool
#-----------------------------------------------------------------------------
@@ -33,8 +30,7 @@ class ButtonWidget(DOMWidget):
def __init__(self, **kwargs):
"""Constructor"""
super(ButtonWidget, self).__init__(**kwargs)
-
- self._click_handlers = []
+ self._click_handlers = CallbackDispatcher(acceptable_nargs=[0, 1])
self.on_msg(self._handle_button_msg)
def on_click(self, callback, remove=False):
@@ -50,10 +46,7 @@ class ButtonWidget(DOMWidget):
----------
remove : bool (optional)
Set to true to remove the callback from the list of callbacks."""
- if remove:
- self._click_handlers.remove(callback)
- elif not callback in self._click_handlers:
- self._click_handlers.append(callback)
+ self._click_handlers.register_callback(callback, remove=remove)
def _handle_button_msg(self, content):
"""Handle a msg from the front-end.
@@ -63,26 +56,5 @@ class ButtonWidget(DOMWidget):
content: dict
Content of the msg."""
if 'event' in content and content['event'] == 'click':
- self._handle_click()
-
- def _handle_click(self):
- """Handles when the button has been clicked.
-
- Fires on_click callbacks when appropriate."""
- for handler in self._click_handlers:
- if callable(handler):
- argspec = inspect.getargspec(handler)
- nargs = len(argspec[0])
-
- # Bound methods have an additional 'self' argument
- if isinstance(handler, types.MethodType):
- nargs -= 1
-
- # Call the callback
- if nargs == 0:
- handler()
- elif nargs == 1:
- handler(self)
- else:
- raise TypeError('ButtonWidget click callback must ' \
- 'accept 0 or 1 arguments.')
+ self._click_handlers()
+ self._click_handlers(self)
diff --git a/IPython/html/widgets/widget_string.py b/IPython/html/widgets/widget_string.py
index ad536838a..e92f1fffc 100644
--- a/IPython/html/widgets/widget_string.py
+++ b/IPython/html/widgets/widget_string.py
@@ -13,10 +13,7 @@ Represents a unicode string using a widget.
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
-import inspect
-import types
-
-from .widget import DOMWidget
+from .widget import DOMWidget, CallbackDispatcher
from IPython.utils.traitlets import Unicode, Bool, List
#-----------------------------------------------------------------------------
@@ -47,7 +44,7 @@ class TextBoxWidget(HTMLWidget):
def __init__(self, **kwargs):
super(TextBoxWidget, self).__init__(**kwargs)
- self._submission_callbacks = []
+ self._submission_callbacks = CallbackDispatcher(acceptable_nargs=[0, 1])
self.on_msg(self._handle_string_msg)
def _handle_string_msg(self, content):
@@ -58,8 +55,8 @@ class TextBoxWidget(HTMLWidget):
content: dict
Content of the msg."""
if 'event' in content and content['event'] == 'submit':
- for handler in self._submission_callbacks:
- handler(self)
+ self._submission_callbacks()
+ self._submission_callbacks(self)
def on_submit(self, callback, remove=False):
"""(Un)Register a callback to handle text submission.
@@ -74,22 +71,4 @@ class TextBoxWidget(HTMLWidget):
callback(sender)
remove: bool (optional)
Whether or not to unregister the callback"""
- if remove and callback in self._submission_callbacks:
- self._submission_callbacks.remove(callback)
- elif not remove and not callback in self._submission_callbacks:
- if callable(callback):
- argspec = inspect.getargspec(callback)
- nargs = len(argspec[0])
-
- # Bound methods have an additional 'self' argument
- if isinstance(callback, types.MethodType):
- nargs -= 1
-
- # Call the callback
- if nargs == 0:
- self._submission_callbacks.append(lambda sender: callback())
- elif nargs == 1:
- self._submission_callbacks.append(callback)
- else:
- raise TypeError('TextBoxWidget submit callback must ' \
- 'accept 0 or 1 arguments.')
+ self._submission_callbacks.register_callback(callback, remove=remove)