##// END OF EJS Templates
Remove residual tabs
Remove residual tabs

File last commit:

r14610:b25d6b37
r14651:a6ff4f47
Show More
widget.py
435 lines | 15.3 KiB | text/x-python | PythonLexer
Jonathan Frederic
Dev meeting widget review day 1
r14586 """Base Widget class. Allows user to create widgets in the back-end that render
in the IPython notebook front-end.
Jonathan Frederic
Cleaned up Python widget code.
r14283 """
#-----------------------------------------------------------------------------
# Copyright (c) 2013, the IPython Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
Jonathan Frederic
Implement a context manager as a property locking mechanism in Widget.
r14579 from contextlib import contextmanager
Jonathan Frederic
Fix: added inspect import to widget.py
r14341 import inspect
Jonathan Frederic
Added missing types import
r14344 import types
Jonathan Frederic
Added widget.py
r14223
Jonathan Frederic
Basic display logic...
r14229 from IPython.kernel.comm import Comm
from IPython.config import LoggingConfigurable
Paul Ivanov
remove unused imports
r14584 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool
Jonathan Frederic
Updates to widget.py...
r14232 from IPython.utils.py3compat import string_types
Jonathan Frederic
Cleaned up Python widget code.
r14283 #-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
Jonathan Frederic
s/Widget/DOMWidget s/BaseWidget/Widget
r14540 class Widget(LoggingConfigurable):
Jonathan Frederic
Added widget.py
r14223
Jonathan Frederic
Added event for widget construction
r14478 # Shared declarations (Class level)
widget_construction_callback = None
Jonathan Frederic
Dev meeting widget review day 1
r14586 widgets = {}
Jonathan Frederic
Added event for widget construction
r14478
def on_widget_constructed(callback):
Jonathan Frederic
More PEP8 changes
r14607 """Registers a callback to be called when a widget is constructed.
The callback must have the following signature:
Jonathan Frederic
Added event for widget construction
r14478 callback(widget)"""
Jonathan Frederic
s/Widget/DOMWidget s/BaseWidget/Widget
r14540 Widget.widget_construction_callback = callback
Jonathan Frederic
Added event for widget construction
r14478
Jonathan Frederic
s/_handle_widget_constructed/_call_widget_constructed
r14542 def _call_widget_constructed(widget):
Jonathan Frederic
Added event for widget construction
r14478 """Class method, called when a widget is constructed."""
Jonathan Frederic
s/Widget/DOMWidget s/BaseWidget/Widget
r14540 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
Widget.widget_construction_callback(widget)
Jonathan Frederic
Added event for widget construction
r14478
# Public declarations (Instance level)
Jonathan Frederic
Everyone uses one model
r14591 model_name = Unicode('WidgetModel', help="""Name of the backbone model
Jonathan Frederic
Dev meeting widget review day 1
r14586 registered in the front-end to create and sync this widget with.""")
view_name = Unicode(help="""Default view registered in the front-end
Jonathan Frederic
sync=True isntead of a keys list
r14588 to use to represent the widget.""", sync=True)
Jonathan Frederic
Added add_class and remove_class methods.
r14313
Jonathan Frederic
Dev meeting widget review day 1
r14586 @contextmanager
def property_lock(self, key, value):
"""Lock a property-value pair.
NOTE: This, in addition to the single lock for all state changes, is
flawed. In the future we may want to look into buffering state changes
back to the front-end."""
self._property_lock = (key, value)
try:
yield
finally:
self._property_lock = (None, None)
def should_send_property(self, key, value):
"""Check the property lock (property_lock)"""
return key != self._property_lock[0] or \
value != self._property_lock[1]
Jonathan Frederic
sync=True isntead of a keys list
r14588 @property
def keys(self):
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
Jonathan Frederic
Cleaned up Python widget code.
r14283 # Private/protected declarations
Jason Grout
Intermediate changes to javascript side of backbone widgets
r14486 _comm = Instance('IPython.kernel.comm.Comm')
Jonathan Frederic
Added widget.py
r14223
Jonathan Frederic
Allow parent to be set after construction......
r14310 def __init__(self, **kwargs):
Jonathan Frederic
More PEP8 changes
r14607 """Public constructor"""
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 self.closed = False
Jonathan Frederic
Dev meeting widget review day 1
r14586 self._property_lock = (None, None)
Jonathan Frederic
Added on_display callback
r14330 self._display_callbacks = []
Jonathan Frederic
Added support for custom widget msgs
r14387 self._msg_callbacks = []
Jonathan Frederic
sync=True isntead of a keys list
r14588 self._keys = None
Jonathan Frederic
s/Widget/DOMWidget s/BaseWidget/Widget
r14540 super(Widget, self).__init__(**kwargs)
Jonathan Frederic
Added event for widget construction
r14478
Jason Grout
Remove the automatic _children_attr and _children_lists_attr....
r14487 self.on_trait_change(self._handle_property_changed, self.keys)
Jonathan Frederic
s/_handle_widget_constructed/_call_widget_constructed
r14542 Widget._call_widget_constructed(self)
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
Added widget.py
r14223 def __del__(self):
Jonathan Frederic
Cleaned up Python widget code.
r14283 """Object disposal"""
Jonathan Frederic
Added widget.py
r14223 self.close()
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
Added widget.py
r14223 def close(self):
Jonathan Frederic
More PEP8 changes
r14607 """Close method.
Closes the widget which closes the underlying comm.
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 When the comm is closed, all of the widget views are automatically
Jonathan Frederic
Dev meeting widget review day 1
r14586 removed from the front-end."""
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 if not self.closed:
Jonathan Frederic
Dev meeting widget review day 1
r14586 self._comm.close()
self._close()
def _close(self):
"""Unsafe close"""
del Widget.widgets[self.model_id]
self._comm = None
self.closed = True
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 @property
def comm(self):
if self._comm is None:
Jonathan Frederic
Dev meeting widget review day 1
r14586 # Create a comm.
self._comm = Comm(target_name=self.model_name)
self._comm.on_msg(self._handle_msg)
self._comm.on_close(self._close)
Widget.widgets[self.model_id] = self
# first update
self.send_state()
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 return self._comm
Jonathan Frederic
Re-decoupled comm_id from widget models
r14512
@property
def model_id(self):
Jonathan Frederic
Fixed typo in model_id property
r14527 return self.comm.comm_id
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
Cleaned up Python widget code.
r14283 # Event handlers
Jonathan Frederic
Added widget.py
r14223 def _handle_msg(self, msg):
Jonathan Frederic
Dev meeting widget review day 1
r14586 """Called when a msg is received from the front-end"""
Jonathan Frederic
Added support for custom widget msgs
r14387 data = msg['content']['data']
Jonathan Frederic
Added `method` property to messages from the front-end
r14432 method = data['method']
Jonathan Frederic
Dev meeting widget review day 1
r14586 if not method in ['backbone', 'custom']:
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
Jonathan Frederic
Many checks off the todo list, test fixes
r14583
Jonathan Frederic
Removed ViewWidget
r14538 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
if method == 'backbone' and 'sync_data' in data:
sync_data = data['sync_data']
Paul Ivanov
fix typos
r14585 self._handle_receive_state(sync_data) # handles all methods
Jonathan Frederic
Added support for custom widget msgs
r14387
# Handle a custom msg from the front-end
Jonathan Frederic
Added `method` property to messages from the front-end
r14432 elif method == 'custom':
if 'custom_content' in data:
self._handle_custom_msg(data['custom_content'])
Jonathan Frederic
Added support for custom widget msgs
r14387
Paul Ivanov
fix typos
r14585 def _handle_receive_state(self, sync_data):
Jonathan Frederic
Dev meeting widget review day 1
r14586 """Called when a state is received from the front-end."""
Jonathan Frederic
re-order handle custom msg and handle recieve state
r14558 for name in self.keys:
if name in sync_data:
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 value = self._unpack_widgets(sync_data[name])
Jonathan Frederic
Dev meeting widget review day 1
r14586 with self.property_lock(name, value):
Jonathan Frederic
Implement a context manager as a property locking mechanism in Widget.
r14579 setattr(self, name, value)
Jonathan Frederic
re-order handle custom msg and handle recieve state
r14558
Jonathan Frederic
Added support for custom widget msgs
r14387 def _handle_custom_msg(self, content):
Paul Ivanov
fix typos
r14585 """Called when a custom msg is received."""
Jonathan Frederic
Added support for custom widget msgs
r14387 for handler in self._msg_callbacks:
Jonathan Frederic
Dev meeting widget review day 1
r14586 handler(self, content)
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
Added widget.py
r14223 def _handle_property_changed(self, name, old, new):
Jason Grout
Intermediate changes to javascript side of backbone widgets
r14486 """Called when a property has been changed."""
Jonathan Frederic
Fixed a bug that didn't allow callbacks to set a property...
r14392 # Make sure this isn't information that the front-end just sent us.
Jonathan Frederic
A lot of bug fixes......
r14594 if self.should_send_property(name, new):
Jonathan Frederic
Dev meeting widget review day 1
r14586 # Send new state to front-end
Jonathan Frederic
Add state packet delta compression.
r14273 self.send_state(key=name)
Jonathan Frederic
Updates to widget.py...
r14232
Jonathan Frederic
Display handler now supports full kwargs
r14484 def _handle_displayed(self, **kwargs):
Jonathan Frederic
remove 3rd callback type from on_displayed
r14550 """Called when a view has been displayed for this widget instance"""
Jonathan Frederic
Added support for custom widget msgs
r14387 for handler in self._display_callbacks:
Jonathan Frederic
Dev meeting widget review day 1
r14586 handler(self, **kwargs)
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
Cleaned up Python widget code.
r14283 # Public methods
def send_state(self, key=None):
Jonathan Frederic
Dev meeting widget review day 1
r14586 """Sends the widget state, or a piece of it, to the front-end.
Jonathan Frederic
Cleaned up Python widget code.
r14283
Parameters
----------
key : unicode (optional)
Jonathan Frederic
Dev meeting widget review day 1
r14586 A single property's name to sync with the front-end.
Jonathan Frederic
Cleaned up Python widget code.
r14283 """
Jonathan Frederic
Halign dict colons
r14610 self._send({
"method" : "update",
"state" : self.get_state()
})
Jonathan Frederic
Cleaned up Python widget code.
r14283
Jason Grout
Intermediate changes to javascript side of backbone widgets
r14486 def get_state(self, key=None):
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 """Gets the widget state, or a piece of it.
Jonathan Frederic
Cleaned up Python widget code.
r14283
Parameters
----------
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 key : unicode (optional)
A single property's name to get.
Jonathan Frederic
Cleaned up Python widget code.
r14283 """
Jonathan Frederic
Dev meeting widget review day 1
r14586 keys = self.keys if key is None else [key]
return {k: self._pack_widgets(getattr(self, k)) for k in keys}
Jonathan Frederic
Cleaned up Python widget code.
r14283
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 def _pack_widgets(self, values):
Jonathan Frederic
More PEP8 changes
r14607 """Recursively converts all widget instances to model id strings.
Jonathan Frederic
Many checks off the todo list, test fixes
r14583
Children widgets will be stored and transmitted to the front-end by
their model ids."""
if isinstance(values, dict):
new_dict = {}
Jonathan Frederic
Dev meeting widget review day 1
r14586 for key, value in values.items():
new_dict[key] = self._pack_widgets(value)
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 return new_dict
elif isinstance(values, list):
new_list = []
for value in values:
new_list.append(self._pack_widgets(value))
return new_list
elif isinstance(values, Widget):
return values.model_id
else:
return values
def _unpack_widgets(self, values):
Jonathan Frederic
More PEP8 changes
r14607 """Recursively converts all model id strings to widget instances.
Jonathan Frederic
Many checks off the todo list, test fixes
r14583
Children widgets will be stored and transmitted to the front-end by
their model ids."""
if isinstance(values, dict):
new_dict = {}
Jonathan Frederic
Dev meeting widget review day 1
r14586 for key, values in values.items():
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 new_dict[key] = self._unpack_widgets(values[key])
return new_dict
elif isinstance(values, list):
new_list = []
for value in values:
new_list.append(self._unpack_widgets(value))
return new_list
elif isinstance(values, string_types):
Jonathan Frederic
More fixes
r14595 if values in Widget.widgets:
return Widget.widgets[values]
Jonathan Frederic
Dev meeting widget review day 1
r14586 else:
return values
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 else:
return values
Jonathan Frederic
Added support for custom widget msgs
r14387 def send(self, content):
"""Sends a custom msg to the widget model in the front-end.
Parameters
----------
content : dict
Content of the message to send.
"""
Jonathan Frederic
Dev meeting widget review day 1
r14586 self._send({"method": "custom", "custom_content": content})
Jonathan Frederic
Added support for custom widget msgs
r14387
Jonathan Frederic
Dev meeting widget review day 1
r14586 def on_msg(self, callback, remove=False):
Jonathan Frederic
More PEP8 changes
r14607 """(Un)Register a custom msg recieve callback.
Jonathan Frederic
Added support for custom widget msgs
r14387
Parameters
----------
callback: method handler
Can have a signature of:
- callback(content)
- callback(sender, content)
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:
Jonathan Frederic
Dev meeting widget review day 1
r14586 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.')
Jonathan Frederic
Added support for custom widget msgs
r14387
Jonathan Frederic
Added on_display callback
r14330 def on_displayed(self, callback, remove=False):
Jonathan Frederic
More PEP8 changes
r14607 """(Un)Register a widget displayed callback.
Jonathan Frederic
Added on_display callback
r14330
Jonathan Frederic
Fixed doc string comments, removed extra space
r14332 Parameters
----------
Jonathan Frederic
Added on_display callback
r14330 callback: method handler
Can have a signature of:
Jonathan Frederic
Display handler now supports full kwargs
r14484 - callback(sender, **kwargs)
kwargs from display call passed through without modification.
Jonathan Frederic
Added on_display callback
r14330 remove: bool
True if the callback should be unregistered."""
Jonathan Frederic
Added support for custom widget msgs
r14387 if remove and callback in self._display_callbacks:
Jonathan Frederic
Added on_display callback
r14330 self._display_callbacks.remove(callback)
Jonathan Frederic
Added support for custom widget msgs
r14387 elif not remove and not callback in self._display_callbacks:
Jonathan Frederic
Dev meeting widget review day 1
r14586 if callable(handler):
self._display_callbacks.append(callback)
else:
raise Exception('Callback must be callable.')
Jonathan Frederic
Added on_display callback
r14330
Jonathan Frederic
Cleaned up Python widget code.
r14283 # Support methods
Jonathan Frederic
Dev meeting widget review day 1
r14586 def _ipython_display_(self, **kwargs):
Jonathan Frederic
More PEP8 changes
r14607 """Called when `IPython.display.display` is called on the widget."""
Jonathan Frederic
Fixed _send so it can open a comm if needed....
r14548 # Show view. By sending a display message, the comm is opened and the
# initial state is sent.
Jonathan Frederic
Remove view_name from display
r14549 self._send({"method": "display"})
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 self._handle_displayed(**kwargs)
Jonathan Frederic
Decoupled Python Widget from Comm...
r14479
def _send(self, msg):
Jonathan Frederic
More PEP8 changes
r14607 """Sends a message to the model in the front-end."""
Jonathan Frederic
Fixed _send so it can open a comm if needed....
r14548 self.comm.send(msg)
Jonathan Frederic
Moved view widget into widget.py
r14516
Jonathan Frederic
s/Widget/DOMWidget s/BaseWidget/Widget
r14540 class DOMWidget(Widget):
Jonathan Frederic
Added sync= attr to DOMWidget
r14589 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
# Private/protected declarations
Jonathan Frederic
Added sync= attr to DOMWidget
r14589 _css = Dict(sync=True) # Internal CSS property dict
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
def get_css(self, key, selector=""):
Jonathan Frederic
Add a comment that explains the notion of the default element...
r14581 """Get a CSS property of the widget.
Note: This function does not actually request the CSS from the
front-end; Only properties that have been set with set_css can be read.
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Parameters
----------
key: unicode
CSS key
selector: unicode (optional)
JQuery selector used when the CSS key/value was set.
"""
if selector in self._css and key in self._css[selector]:
return self._css[selector][key]
else:
return None
def set_css(self, *args, **kwargs):
Jonathan Frederic
Add a comment that explains the notion of the default element...
r14581 """Set one or more CSS properties of the widget.
This function has two signatures:
Jonathan Frederic
Fixed comments for optional kwargs so they are redundant.
r14551 - set_css(css_dict, selector='')
- set_css(key, value, selector='')
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Parameters
----------
css_dict : dict
CSS key/value pairs to apply
key: unicode
CSS key
value
CSS value
selector: unicode (optional)
Jonathan Frederic
Add a comment that explains the notion of the default element...
r14581 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
element is an attribute unique to each view, which is a DOM element
of the view that should be styled with common CSS (see
`$el_to_style` in the Javascript code).
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 """
selector = kwargs.get('selector', '')
Jonathan Frederic
Fixed *almost* all of the test-detected bugs
r14596 if not selector in self._css:
self._css[selector] = {}
Jonathan Frederic
Fixed comments for optional kwargs so they are redundant.
r14551 # Signature 1: set_css(css_dict, selector='')
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 if len(args) == 1:
if isinstance(args[0], dict):
for (key, value) in args[0].items():
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 if not (key in self._css[selector] and value == self._css[selector][key]):
Jonathan Frederic
send_state only once for dict signature of set_css
r14552 self._css[selector][key] = value
self.send_state('_css')
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 else:
raise Exception('css_dict must be a dict.')
Jonathan Frederic
Fixed comments for optional kwargs so they are redundant.
r14551 # Signature 2: set_css(key, value, selector='')
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 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]
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 if not (key in self._css[selector] and value == self._css[selector][key]):
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 self._css[selector][key] = value
self.send_state('_css') # Send new state to client.
else:
raise Exception('set_css only accepts 1-3 arguments')
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 def add_class(self, class_names, selector=""):
Jonathan Frederic
More PEP8 changes
r14607 """Add class[es] to a DOM element.
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Parameters
----------
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_names: unicode or list
Class name(s) to add to the DOM element(s).
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 selector: unicode (optional)
JQuery selector to select the DOM element(s) that the class(es) will
be added to.
"""
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_list = class_names
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 if isinstance(class_list, list):
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_list = ' '.join(class_list)
Jonathan Frederic
Halign dict colons
r14610 self.send({
"msg_type" : "add_class",
"class_list" : class_list,
"selector" : selector
})
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 def remove_class(self, class_names, selector=""):
Jonathan Frederic
More PEP8 changes
r14607 """Remove class[es] from a DOM element.
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485
Parameters
----------
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_names: unicode or list
Class name(s) to remove from the DOM element(s).
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 selector: unicode (optional)
JQuery selector to select the DOM element(s) that the class(es) will
be removed from.
"""
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_list = class_names
Jonathan Frederic
Many checks off the todo list, test fixes
r14583 if isinstance(class_list, list):
Jonathan Frederic
add/remove_class now can accept a list of classes
r14539 class_list = ' '.join(class_list)
Jonathan Frederic
Halign dict colons
r14610 self.send({
"msg_type" : "remove_class",
"class_list" : class_list,
"selector" : selector,
})