diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index 3c4fba829..c0684550a 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -13,37 +13,28 @@ in the IPython notebook front-end. # Imports #----------------------------------------------------------------------------- from contextlib import contextmanager -import inspect -import types from IPython.kernel.comm import Comm from IPython.config import LoggingConfigurable -from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List +from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple from IPython.utils.py3compat import string_types #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- 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) + """A structure for registering and running callbacks""" + callbacks = List() + + def __call__(self, *args, **kwargs): + """Call all of the registered callbacks.""" value = None - if nargs in self.callbacks: - for callback in self.callbacks[nargs]: - local_value = callback(*pargs, **kwargs) + for callback in self.callbacks: + try: + local_value = callback(*args, **kwargs) + except Exception as e: + self.log.warn("Exception in callback %s: %s", callback, e) + else: value = local_value if local_value is not None else value return value @@ -53,69 +44,37 @@ class CallbackDispatcher(LoggingConfigurable): Parameters ---------- callback: method handle - Method to be registered or unregisted. + Method to be registered or unregistered. 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] - + Whether to unregister the callback.""" + # (Un)Register the callback. - if remove and callback in callback_list: - callback_list.remove(callback) - elif 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) - if argspec[0] is None: - nargs = 0 - elif argspec[3] is None: - nargs = len(argspec[0]) # Only count vargs! - else: - nargs = len(argspec[0]) - len(argspec[3]) # Subtract number of defaults. - - # Bound methods have an additional 'self' argument - if isinstance(callback, types.MethodType): - nargs -= 1 - return nargs - else: - raise TypeError('Callback must be callable.') + if remove and callback in self.callbacks: + self.callbacks.remove(callback) + elif not remove and callback not in self.callbacks: + self.callbacks.append(callback) class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- - widget_construction_callback = None + _widget_construction_callback = None widgets = {} + @staticmethod def on_widget_constructed(callback): - """Registers a callback to be called when a widget is constructed. + """Registers a callback to be called when a widget is constructed. The callback must have the following signature: callback(widget)""" - Widget.widget_construction_callback = callback + Widget._widget_construction_callback = callback + @staticmethod def _call_widget_constructed(widget): - """Class method, called when a widget is constructed.""" - if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback): - Widget.widget_construction_callback(widget) + """Static method, called when a widget is constructed.""" + if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback): + Widget._widget_construction_callback(widget) #------------------------------------------------------------------------- # Traits @@ -125,20 +84,23 @@ class Widget(LoggingConfigurable): _view_name = Unicode(help="""Default view registered in the front-end to use to represent the widget.""", sync=True) _comm = Instance('IPython.kernel.comm.Comm') - + + closed = Bool(False) + + keys = List() + def _keys_default(self): + return [name for name in self.traits(sync=True)] + + _property_lock = Tuple((None, None)) + + _display_callbacks = Instance(CallbackDispatcher, ()) + _msg_callbacks = Instance(CallbackDispatcher, ()) + #------------------------------------------------------------------------- # (Con/de)structor - #------------------------------------------------------------------------- + #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" - self.closed = False - - self._property_lock = (None, None) - 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) @@ -150,16 +112,7 @@ class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Properties - #------------------------------------------------------------------------- - @property - def keys(self): - """Gets a list of the traitlets that should be synced with the front-end.""" - if self._keys is None: - self._keys = [] - for trait_name in self.trait_names(): - if self.trait_metadata(trait_name, 'sync'): - self._keys.append(trait_name) - return self._keys + #------------------------------------------------------------------------- @property def comm(self): @@ -186,15 +139,21 @@ class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Methods - #------------------------------------------------------------------------- + #------------------------------------------------------------------------- + def _close(self): + """Private close - cleanup objects, registry entries""" + del Widget.widgets[self.model_id] + self._comm = None + self.closed = True + def close(self): - """Close method. + """Close method. Closes the widget which closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if not self.closed: - self._comm.close() + self._comm.close() self._close() def send_state(self, key=None): @@ -232,14 +191,13 @@ class Widget(LoggingConfigurable): self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): - """(Un)Register a custom msg recieve callback. + """(Un)Register a custom msg receive callback. Parameters ---------- - callback: method handler - Can have a signature of: - - callback(content) Signature 1 - - callback(sender, content) Signature 2 + callback: callable + callback will be passed two arguments when a message arrives: + callback(widget, content) remove: bool True if the callback should be unregistered.""" self._msg_callbacks.register_callback(callback, remove=remove) @@ -250,9 +208,9 @@ class Widget(LoggingConfigurable): Parameters ---------- callback: method handler - Can have a signature of: - - callback(sender, **kwargs) - kwargs from display call passed through without modification. + Must have a signature of: + callback(widget, **kwargs) + kwargs from display are passed through without modification. remove: bool True if the callback should be unregistered.""" self._display_callbacks.register_callback(callback, remove=remove) @@ -278,12 +236,6 @@ class Widget(LoggingConfigurable): return key != self._property_lock[0] or \ value != self._property_lock[1] - def _close(self): - """Unsafe close""" - del Widget.widgets[self.model_id] - self._comm = None - self.closed = True - # Event handlers def _handle_msg(self, msg): """Called when a msg is received from the front-end""" @@ -312,8 +264,7 @@ class Widget(LoggingConfigurable): def _handle_custom_msg(self, content): """Called when a custom msg is received.""" - self._msg_callbacks(content) # Signature 1 - self._msg_callbacks(self, content) # Signature 2 + self._msg_callbacks(self, content) def _handle_property_changed(self, name, old, new): """Called when a property has been changed.""" @@ -324,7 +275,7 @@ class Widget(LoggingConfigurable): def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" - self._display_callbacks(**kwargs) + self._display_callbacks(self, **kwargs) def _pack_widgets(self, x): """Recursively converts all widget instances to model id strings. @@ -367,7 +318,7 @@ class Widget(LoggingConfigurable): class DOMWidget(Widget): - visible = Bool(True, help="Whether or not the widget is visible.", sync=True) + visible = Bool(True, help="Whether the widget is visible.", sync=True) _css = Dict(sync=True) # Internal CSS property dict def get_css(self, key, selector=""): @@ -388,7 +339,7 @@ class DOMWidget(Widget): else: return None - def set_css(self, *args, **kwargs): + def set_css(self, dict_or_key, value=None, selector=''): """Set one or more CSS properties of the widget. This function has two signatures: @@ -401,9 +352,9 @@ class DOMWidget(Widget): CSS key/value pairs to apply key: unicode CSS key - value + value: CSS value - selector: unicode (optional) + selector: unicode (optional, kwarg only) JQuery selector to use to apply the CSS key/value. If no selector is provided, an empty selector is used. An empty selector makes the front-end try to apply the css to a default element. The default @@ -411,37 +362,19 @@ class DOMWidget(Widget): of the view that should be styled with common CSS (see `$el_to_style` in the Javascript code). """ - selector = kwargs.get('selector', '') if not selector in self._css: self._css[selector] = {} - - # Signature 1: set_css(css_dict, selector='') - if len(args) == 1: - if isinstance(args[0], dict): - for (key, value) in args[0].items(): - if not (key in self._css[selector] and value == self._css[selector][key]): - self._css[selector][key] = value - self.send_state('_css') - else: - raise Exception('css_dict must be a dict.') - - # Signature 2: set_css(key, value, selector='') - elif len(args) == 2 or len(args) == 3: - - # Selector can be a positional arg if it's the 3rd value - if len(args) == 3: - selector = args[2] - if selector not in self._css: - self._css[selector] = {} - - # Only update the property if it has changed. - key = args[0] - value = args[1] - if not (key in self._css[selector] and value == self._css[selector][key]): - self._css[selector][key] = value - self.send_state('_css') # Send new state to client. + my_css = self._css[selector] + + if value is None: + css_dict = dict_or_key else: - raise Exception('set_css only accepts 1-3 arguments') + css_dict = {dict_or_key: value} + + for (key, value) in css_dict.items(): + if not (key in my_css and value == my_css[key]): + my_css[key] = value + self.send_state('_css') def add_class(self, class_names, selector=""): """Add class[es] to a DOM element. diff --git a/IPython/html/widgets/widget_button.py b/IPython/html/widgets/widget_button.py index c2f552e4c..3fdfe723b 100644 --- a/IPython/html/widgets/widget_button.py +++ b/IPython/html/widgets/widget_button.py @@ -30,17 +30,14 @@ class ButtonWidget(DOMWidget): def __init__(self, **kwargs): """Constructor""" super(ButtonWidget, self).__init__(**kwargs) - self._click_handlers = CallbackDispatcher(acceptable_nargs=[0, 1]) + self._click_handlers = CallbackDispatcher() self.on_msg(self._handle_button_msg) def on_click(self, callback, remove=False): - """Register a callback to execute when the button is clicked. + """Register a callback to execute when the button is clicked. - The callback can either accept no parameters or one sender parameter: - - callback() - - callback(sender) - If the callback has a sender parameter, the ButtonWidget instance that - called the callback will be passed into the method as the sender. + The callback will be called with one argument, + the clicked button widget instance. Parameters ---------- @@ -48,13 +45,12 @@ class ButtonWidget(DOMWidget): Set to true to remove the callback from the list of callbacks.""" self._click_handlers.register_callback(callback, remove=remove) - def _handle_button_msg(self, content): + def _handle_button_msg(self, _, content): """Handle a msg from the front-end. Parameters ---------- content: dict Content of the msg.""" - if 'event' in content and content['event'] == 'click': - self._click_handlers() + if content.get('event', '') == 'click': self._click_handlers(self) diff --git a/IPython/html/widgets/widget_string.py b/IPython/html/widgets/widget_string.py index 52020b6a1..d0a2812ab 100644 --- a/IPython/html/widgets/widget_string.py +++ b/IPython/html/widgets/widget_string.py @@ -26,7 +26,7 @@ class _StringWidget(DOMWidget): class HTMLWidget(_StringWidget): - _view_name = Unicode('HTMLView', sync=True) + _view_name = Unicode('HTMLView', sync=True) class LatexWidget(_StringWidget): @@ -45,18 +45,17 @@ class TextBoxWidget(_StringWidget): def __init__(self, **kwargs): super(TextBoxWidget, self).__init__(**kwargs) - self._submission_callbacks = CallbackDispatcher(acceptable_nargs=[0, 1]) + self._submission_callbacks = CallbackDispatcher() self.on_msg(self._handle_string_msg) - def _handle_string_msg(self, content): + def _handle_string_msg(self, _, content): """Handle a msg from the front-end. Parameters ---------- content: dict Content of the msg.""" - if 'event' in content and content['event'] == 'submit': - self._submission_callbacks() + if content.get('event', '') == 'submit': self._submission_callbacks(self) def on_submit(self, callback, remove=False): @@ -65,11 +64,9 @@ class TextBoxWidget(_StringWidget): Triggered when the user clicks enter. Parameters - callback: Method handle - Function to be called when the text has been submitted. Function - can have two possible signatures: - callback() - callback(sender) + ---------- + callback: callable + Will be called with exactly one argument: the Widget instance remove: bool (optional) - Whether or not to unregister the callback""" + Whether to unregister the callback""" self._submission_callbacks.register_callback(callback, remove=remove)