review pass on Python-side of widgets

- remove variable signature complexity
- use a few more traitlets as appropriate
MinRK 12 years ago
parent dd2f4a25aa
commit 36db60eacb

@ -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.

@ -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)

@ -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)

Loading…
Cancel
Save