##// END OF EJS Templates
'create' should be handled in sync
'create' should be handled in sync

File last commit:

r14659:11ca541c
r14661:5f40d5df
Show More
widget.py
478 lines | 17.4 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
Jonathan Frederic
Added new CallbackDispatcher class
r14658 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List
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
Added new CallbackDispatcher class
r14658 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.')
Jonathan Frederic
Added widget.py
r14223
Jonathan Frederic
Added new CallbackDispatcher class
r14658
class Widget(LoggingConfigurable):
Jonathan Frederic
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
# Class attributes
#-------------------------------------------------------------------------
Jonathan Frederic
Added event for widget construction
r14478 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
Jonathan Frederic
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
# Traits
#-------------------------------------------------------------------------
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)
Jason Grout
Intermediate changes to javascript side of backbone widgets
r14486 _comm = Instance('IPython.kernel.comm.Comm')
Jonathan Frederic
Reorganized attrs in widget.py
r14653
#-------------------------------------------------------------------------
# (Con/de)structor
#-------------------------------------------------------------------------
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
Added new CallbackDispatcher class
r14658
Jonathan Frederic
Dev meeting widget review day 1
r14586 self._property_lock = (None, None)
Jonathan Frederic
sync=True isntead of a keys list
r14588 self._keys = None
Jonathan Frederic
Added new CallbackDispatcher class
r14658
self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
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
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
# Properties
#-------------------------------------------------------------------------
@property
def keys(self):
Jonathan Frederic
Added doc strings to properties in widget.py
r14654 """Gets a list of the traitlets that should be synced with the front-end."""
Jonathan Frederic
Reorganized attrs in widget.py
r14653 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
Dev meeting widget review day 1
r14586
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 @property
def comm(self):
Jonathan Frederic
Added doc strings to properties in widget.py
r14654 """Gets the Comm associated with this widget.
If a Comm doesn't exist yet, a Comm will be created automagically."""
Jason Grout
Separate the display from the models on the python side, creating a BaseWidget class....
r14485 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
Added doc strings to properties in widget.py
r14654 """Gets the model id of this widget.
If a Comm doesn't exist yet, a Comm will be created automagically."""
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
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
# Methods
#-------------------------------------------------------------------------
def close(self):
"""Close method.
Jonathan Frederic
Updates to widget.py...
r14232
Jonathan Frederic
Reorganized attrs in widget.py
r14653 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._close()
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 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
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
s/custom_content/content
r14655 self._send({"method": "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:
Jonathan Frederic
Added new CallbackDispatcher class
r14658 - callback(content) Signature 1
- callback(sender, content) Signature 2
Jonathan Frederic
Added support for custom widget msgs
r14387 remove: bool
True if the callback should be unregistered."""
Jonathan Frederic
Added new CallbackDispatcher class
r14658 self._msg_callbacks.register_callback(callback, remove=remove)
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 new CallbackDispatcher class
r14658 self._display_callbacks.register_callback(callback, remove=remove)
Jonathan Frederic
Added on_display callback
r14330
Jonathan Frederic
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
Jonathan Frederic
Cleaned up Python widget code.
r14283 # Support methods
Jonathan Frederic
Reorganized attrs in widget.py
r14653 #-------------------------------------------------------------------------
@contextmanager
Jonathan Frederic
Fixed name conflict with _property_lock
r14659 def _lock_property(self, key, value):
Jonathan Frederic
Reorganized attrs in widget.py
r14653 """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]
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"""
data = msg['content']['data']
method = data['method']
if not method in ['backbone', 'custom']:
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
# Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
if method == 'backbone' and 'sync_data' in data:
sync_data = data['sync_data']
self._handle_receive_state(sync_data) # handles all methods
# Handle a custom msg from the front-end
elif method == 'custom':
Jonathan Frederic
s/custom_content/content
r14655 if 'content' in data:
self._handle_custom_msg(data['content'])
Jonathan Frederic
Reorganized attrs in widget.py
r14653
def _handle_receive_state(self, sync_data):
"""Called when a state is received from the front-end."""
for name in self.keys:
if name in sync_data:
value = self._unpack_widgets(sync_data[name])
Jonathan Frederic
Fixed name conflict with _property_lock
r14659 with self._lock_property(name, value):
Jonathan Frederic
Reorganized attrs in widget.py
r14653 setattr(self, name, value)
def _handle_custom_msg(self, content):
"""Called when a custom msg is received."""
Jonathan Frederic
Added new CallbackDispatcher class
r14658 self._msg_callbacks(content) # Signature 1
self._msg_callbacks(self, content) # Signature 2
Jonathan Frederic
Reorganized attrs in widget.py
r14653
def _handle_property_changed(self, name, old, new):
"""Called when a property has been changed."""
# Make sure this isn't information that the front-end just sent us.
if self._should_send_property(name, new):
# Send new state to front-end
self.send_state(key=name)
def _handle_displayed(self, **kwargs):
"""Called when a view has been displayed for this widget instance"""
Jonathan Frederic
Added new CallbackDispatcher class
r14658 self._display_callbacks(**kwargs)
Jonathan Frederic
Reorganized attrs in widget.py
r14653
Jonathan Frederic
ict comprehension and list comprehension in pack/unpack widgets
r14656 def _pack_widgets(self, x):
Jonathan Frederic
Reorganized attrs in widget.py
r14653 """Recursively converts all widget instances to model id strings.
Children widgets will be stored and transmitted to the front-end by
Jonathan Frederic
Document in widget packing that vaues must be JSON-able.
r14657 their model ids. Return value must be JSON-able."""
Jonathan Frederic
ict comprehension and list comprehension in pack/unpack widgets
r14656 if isinstance(x, dict):
return {k: self._pack_widgets(v) for k, v in x.items()}
elif isinstance(x, list):
return [self._pack_widgets(v) for v in x]
elif isinstance(x, Widget):
return x.model_id
Jonathan Frederic
Reorganized attrs in widget.py
r14653 else:
Jonathan Frederic
Document in widget packing that vaues must be JSON-able.
r14657 return x # Value must be JSON-able
Jonathan Frederic
Reorganized attrs in widget.py
r14653
Jonathan Frederic
ict comprehension and list comprehension in pack/unpack widgets
r14656 def _unpack_widgets(self, x):
Jonathan Frederic
Reorganized attrs in widget.py
r14653 """Recursively converts all model id strings to widget instances.
Children widgets will be stored and transmitted to the front-end by
their model ids."""
Jonathan Frederic
ict comprehension and list comprehension in pack/unpack widgets
r14656 if isinstance(x, dict):
return {k: self._unpack_widgets(v) for k, v in x.items()}
elif isinstance(x, list):
return [self._unpack_widgets(v) for v in x]
elif isinstance(x, string_types):
return x if x not in Widget.widgets else Widget.widgets[x]
Jonathan Frederic
Reorganized attrs in widget.py
r14653 else:
Jonathan Frederic
ict comprehension and list comprehension in pack/unpack widgets
r14656 return x
Jonathan Frederic
Reorganized attrs in widget.py
r14653
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)
_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,
})