From 077bd5c6caa76e22e26aa4ea7e3ba4579dff8f26 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Tue, 21 Jan 2014 15:14:27 -0800 Subject: [PATCH] Added new CallbackDispatcher class --- IPython/html/widgets/widget.py | 117 +++++++++++++++++--------- IPython/html/widgets/widget_button.py | 38 ++------- IPython/html/widgets/widget_string.py | 31 ++----- 3 files changed, 89 insertions(+), 97 deletions(-) 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)