Many checks off the todo list, test fixes

This commit is contained in:
Jonathan Frederic 2014-01-13 18:06:19 +00:00
parent f6de685d9f
commit d103c682c5
13 changed files with 199 additions and 131 deletions

View File

@ -95,7 +95,7 @@ function(widget_manager, underscore, backbone){
var value = state[key];
this.key_value_lock = [key, value];
try {
this.set(key, state[key]);
this.set(key, this._unpack_models(value));
} finally {
this.key_value_lock = null;
}
@ -137,32 +137,26 @@ function(widget_manager, underscore, backbone){
// The throttle has been exceeded, buffer the current msg so
// it can be sent once the kernel has finished processing
// some of the existing messages.
if (method=='patch') {
if (this.msg_buffer === null) {
this.msg_buffer = $.extend({}, model_json); // Copy
}
for (attr in options.attrs) {
var value = options.attrs[attr];
if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
this.msg_buffer[attr] = value;
}
}
} else {
if (this.msg_buffer === null) {
this.msg_buffer = $.extend({}, model_json); // Copy
}
for (attr in options.attrs) {
var value = this._pack_models(options.attrs[attr]);
if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
this.msg_buffer[attr] = value;
}
}
} else {
// We haven't exceeded the throttle, send the message like
// normal. If this is a patch operation, just send the
// changes.
var send_json = model_json;
if (method =='patch') {
send_json = {};
for (attr in options.attrs) {
var value = options.attrs[attr];
if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
send_json[attr] = value;
}
send_json = {};
for (attr in options.attrs) {
var value = this._pack_models(options.attrs[attr]);
if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
send_json[attr] = value;
}
}
@ -177,6 +171,37 @@ function(widget_manager, underscore, backbone){
return model_json;
},
_pack_models: function(value) {
if (value instanceof Backbone.Model) {
return value.id;
} else if (value instanceof Object) {
var packed = {};
for (var key in value) {
packed[key] = this._pack_models(value[key]);
}
return packed;
} else {
return value;
}
},
_unpack_models: function(value) {
if (value instanceof Object) {
var unpacked = {};
for (var key in value) {
unpacked[key] = this._unpack_models(value[key]);
}
return unpacked;
} else {
var model = this.widget_manager.get_model(value);
if (model !== null) {
return model;
} else {
return value;
}
}
},
});
@ -196,24 +221,23 @@ function(widget_manager, underscore, backbone){
// triggered on model change
},
child_view: function(model_id, options) {
// create and return a child view, given a model id for a model and (optionally) a view name
child_view: function(child_model, options) {
// create and return a child view, given a model and (optionally) a view name
// if the view name is not given, it defaults to the model's default view attribute
var child_model = this.widget_manager.get_model(model_id);
var child_view = this.widget_manager.create_view(child_model, options);
this.child_views[model_id] = child_view;
var child_view = this.model.widget_manager.create_view(child_model, options);
this.child_views[child_model.id] = child_view;
return child_view;
},
update_child_views: function(old_list, new_list) {
// this function takes an old list and new list of model ids
// this function takes an old list and new list of models
// views in child_views that correspond to deleted ids are deleted
// views corresponding to added ids are added child_views
// delete old views
_.each(_.difference(old_list, new_list), function(element, index, list) {
var view = this.child_views[element];
delete this.child_views[element];
var view = this.child_views[element.id];
delete this.child_views[element.id];
view.remove();
}, this);
@ -247,10 +271,10 @@ function(widget_manager, underscore, backbone){
_.each(_.difference(new_list, old_list), function(item, index, list) {
added_callback(item);
}, this);
}
},
callbacks: function(){
return this.widget_manager.callbacks(this);
return this.model.widget_manager.callbacks(this);
},
render: function(){
@ -271,7 +295,7 @@ function(widget_manager, underscore, backbone){
// TODO: make changes more granular (e.g., trigger on visible:change)
this.model.on('change', this.update, this);
this.model.on('msg:custom', this.on_msg, this);
WidgetView.initialize.apply(this, arguments);
DOMWidgetView.__super__.initialize.apply(this, arguments);
},
on_msg: function(msg) {

View File

@ -64,7 +64,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return CheckboxView.__super__.update.apply(this);
},
});
@ -88,13 +88,13 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
//
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
if (options === undefined || options.updated_view != this) {
if (this.model.get('value')) {
this.$el.addClass('active');
} else {
this.$el.removeClass('active');
}
if (this.model.get('value')) {
this.$el.addClass('active');
} else {
this.$el.removeClass('active');
}
if (options === undefined || options.updated_view != this) {
var disabled = this.model.get('disabled');
this.$el.prop('disabled', disabled);
@ -105,17 +105,17 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.html(description);
}
}
return IPython.DOMWidgetView.update.apply(this);
return ToggleButtonView.__super__.update.apply(this);
},
events: {"click button" : "handleClick"},
events: {"click" : "handleClick"},
// Handles and validates user input.
handleClick: function(e) {
// Calling model.set will trigger all of the other views of the
// model to update.
this.model.set('value', ! $(e.target).hasClass('active'), {updated_view: this});
this.model.set('value', ! $(this.$el).hasClass('active'), {updated_view: this});
this.touch();
},
});

View File

@ -49,7 +49,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.removeAttr('disabled');
}
return IPython.DOMWidgetView.update.apply(this);
return ButtonView.__super__.update.apply(this);
},
events: {

View File

@ -64,7 +64,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
this.$el.empty();
this.update_child_views(old_list, new_list);
_.each(new_list, function(element, index, list) {
this.$el.append(this.child_views[element].$el);
this.$el.append(this.child_views[element.id].$el);
}, this)
},
@ -74,7 +74,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
set_flex_properties(this, this.$el);
return IPython.DOMWidgetView.update.apply(this);
return ContainerView.__super__.update.apply(this);
},
});
@ -258,7 +258,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
this.show();
}
return IPython.DOMWidgetView.update.apply(this);
return ModalView.__super__.update.apply(this);
},
_get_selector_element: function(selector) {
@ -277,7 +277,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
return this.$window.find(selector.substring(6));
}
} else {
return IPython.DOMWidgetView._get_selector_element.apply(this, [selector]);
return ModalView.__super__._get_selector_element.apply(this, [selector]);
}
},

View File

@ -47,7 +47,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
} else {
this.$el.removeAttr('height');
}
return IPython.DOMWidgetView.update.apply(this);
return ImageView.__super__.update.apply(this);
},
});

View File

@ -101,7 +101,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return DropdownView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@ -193,7 +193,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return RadioButtonsView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@ -280,7 +280,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return ToggleButtonsView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@ -364,7 +364,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return ListBoxView.__super__.update.apply(this);
},
// Handle when a value is clicked.

View File

@ -77,7 +77,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
}
}
}
return IPython.DOMWidgetView.update.apply(this);
return AccordionView.__super__.update.apply(this);
},
add_child_view: function(view) {
@ -130,7 +130,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
initialize: function() {
this.containers = [];
IPython.DOMWidgetView.initialize.apply(this, arguments);
TabView.__super__.initialize.apply(this, arguments);
},
render: function(){
@ -181,7 +181,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.select_page(selected_index);
}
}
return IPython.DOMWidgetView.update.apply(this);
return TabView.__super__.update.apply(this);
},
add_child_view: function(view) {

View File

@ -31,7 +31,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
this.$el.html(this.model.get('value'));
return IPython.DOMWidgetView.update.apply(this);
return HTMLView.__super__.update.apply(this);
},
});
@ -54,7 +54,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.html(this.model.get('value'));
MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$el.get(0)]);
return IPython.DOMWidgetView.update.apply(this);
return LatexView.__super__.update.apply(this);
},
});
@ -114,7 +114,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return TextAreaView.__super__.update.apply(this);
},
events: {"keyup textarea": "handleChanging",
@ -173,7 +173,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
return IPython.DOMWidgetView.update.apply(this);
return TextBoxView.__super__.update.apply(this);
},
events: {"keyup input": "handleChanging",

View File

@ -27,7 +27,7 @@ casper.notebook_test(function () {
index = this.append_cell(
'names = [name for name in dir(widgets)' +
' if name.endswith("Widget") and name!= "Widget"]\n' +
' if name.endswith("Widget") and name!= "Widget" and name!= "DOMWidget"]\n' +
'for name in names:\n' +
' print(name)\n');
this.execute_cell_then(index, function(index){
@ -37,7 +37,7 @@ casper.notebook_test(function () {
// suffixed).
var javascript_names = this.evaluate(function () {
names = [];
for (var name in IPython.widget_manager.widget_model_types) {
for (var name in IPython.widget_manager._model_types) {
names.push(name.replace('Model',''));
}
return names;

View File

@ -7,9 +7,15 @@ casper.notebook_test(function () {
this.execute_cell_then(index);
var bool_index = this.append_cell(
'bool_widget = widgets.BoolWidget(description="Title", value=True)\n' +
'display(bool_widget)\n'+
'display(bool_widget, view_name="ToggleButtonView")\n' +
'bool_widgets = [widgets.BoolWidget(description="Title", value=True) for i in range(2)]\n' +
'display(bool_widgets[0])\n' +
'bool_widgets[1].view_name = "ToggleButtonView"\n' +
'display(bool_widgets[1])\n' +
'for widget in bool_widgets:\n' +
' def handle_change(name,old,new):\n' +
' for other_widget in bool_widgets:\n' +
' other_widget.value = new\n' +
' widget.on_trait_change(handle_change, "value")\n' +
'print("Success")');
this.execute_cell_then(bool_index, function(index){
@ -37,21 +43,21 @@ casper.notebook_test(function () {
'Checkbox labeled correctly.');
this.test.assert(this.cell_element_exists(index,
'.widget-area .widget-subarea div button'),
'.widget-area .widget-subarea button'),
'Toggle button exists.');
this.test.assert(this.cell_element_function(index,
'.widget-area .widget-subarea div button', 'html')=="Title",
'.widget-area .widget-subarea button', 'html')=="Title",
'Toggle button labeled correctly.');
this.test.assert(this.cell_element_function(index,
'.widget-area .widget-subarea div button', 'hasClass', ['active']),
'.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is toggled.');
});
index = this.append_cell(
'bool_widget.value = False\n' +
'bool_widgets[0].value = False\n' +
'print("Success")');
this.execute_cell_then(index, function(index){
@ -63,18 +69,18 @@ casper.notebook_test(function () {
'Checkbox is not checked. (1)');
this.test.assert(! this.cell_element_function(bool_index,
'.widget-area .widget-subarea div button', 'hasClass', ['active']),
'.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is not toggled. (1)');
// Try toggling the bool by clicking on the toggle button.
this.cell_element_function(bool_index, '.widget-area .widget-subarea div button', 'click');
this.cell_element_function(bool_index, '.widget-area .widget-subarea button', 'click');
this.test.assert(this.cell_element_function(bool_index,
'.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
'Checkbox is checked. (2)');
this.test.assert(this.cell_element_function(bool_index,
'.widget-area .widget-subarea div button', 'hasClass', ['active']),
'.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is toggled. (2)');
// Try toggling the bool by clicking on the checkbox.
@ -85,7 +91,7 @@ casper.notebook_test(function () {
'Checkbox is not checked. (3)');
this.test.assert(! this.cell_element_function(bool_index,
'.widget-area .widget-subarea div button', 'hasClass', ['active']),
'.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is not toggled. (3)');
});

View File

@ -42,11 +42,16 @@ casper.notebook_test(function () {
}
selection_index = this.append_cell(
'selection = widgets.SelectionWidget(values=["' + selection_values + '"[i] for i in range(4)])\n' +
'display(selection)\n' +
'display(selection, view_name="ToggleButtonsView")\n' +
'display(selection, view_name="RadioButtonsView")\n' +
'display(selection, view_name="ListBoxView")\n' +
'selection = [widgets.SelectionWidget(values=["' + selection_values + '"[i] for i in range(4)]) for j in range(4)]\n' +
'selection[1].view_name="ToggleButtonsView"\n' +
'selection[2].view_name="RadioButtonsView"\n' +
'selection[3].view_name="ListBoxView"\n' +
'[display(selection[i]) for i in range(4)]\n' +
'for widget in selection:\n' +
' def handle_change(name,old,new):\n' +
' for other_widget in selection:\n' +
' other_widget.value = new\n' +
' widget.on_trait_change(handle_change, "value")\n' +
'print("Success")\n');
this.execute_cell_then(selection_index, function(index){
this.test.assert(this.get_output_cell(index).text == 'Success\n',
@ -73,7 +78,8 @@ casper.notebook_test(function () {
});
index = this.append_cell(
'selection.value = "a"\n' +
'for widget in selection:\n' +
' widget.value = "a"\n' +
'print("Success")\n');
this.execute_cell_then(index, function(index){
this.test.assert(this.get_output_cell(index).text == 'Success\n',

View File

@ -7,12 +7,15 @@ casper.notebook_test(function () {
this.execute_cell_then(index);
var string_index = this.append_cell(
'string_widget = widgets.StringWidget()\n' +
'display(string_widget)\n'+
'display(string_widget, view_name="TextAreaView")\n' +
'display(string_widget, view_name="HTMLView")\n' +
'display(string_widget, view_name="LatexView")\n' +
'string_widget.value = "xyz"\n' +
'string_widget = [widgets.StringWidget(), widgets.StringWidget(), widgets.StringWidget(), widgets.StringWidget()]\n' +
'string_widget[0].value = "xyz"\n' +
'string_widget[1].view_name = "TextAreaView"\n' +
'string_widget[1].value = "xyz"\n' +
'string_widget[2].view_name = "HTMLView"\n' +
'string_widget[2].value = "xyz"\n' +
'string_widget[3].view_name = "LatexView"\n' +
'string_widget[3].value = "$\\\\LaTeX{}$"\n' +
'[display(widget) for widget in string_widget]\n'+
'print("Success")');
this.execute_cell_then(string_index, function(index){
@ -39,29 +42,12 @@ casper.notebook_test(function () {
'.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val')=='xyz',
'Python set textbox value.');
this.cell_element_function(index,
'.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val', [''])
this.sendKeys('.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'abc');
this.test.assert(this.cell_element_function(index,
'.widget-area .widget-subarea .widget-hbox textarea', 'val')=='abc',
'Textarea updated to textbox contents.');
this.cell_element_function(index,
'.widget-area .widget-subarea .widget-hbox textarea', 'val', ['']);
this.sendKeys('.widget-area .widget-subarea .widget-hbox textarea', '$\\LaTeX{}$');
this.test.assert(this.cell_element_function(index,
'.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val')=='$\\LaTeX{}$',
'Textbox updated to textarea contents.');
});
this.wait(500); // Wait for change to execute in kernel
index = this.append_cell('print(string_widget.value)');
this.execute_cell_then(index, function(index){
this.test.assert(this.get_output_cell(index).text == '$\\LaTeX{}$\n',
'Python updated with correct string widget value.');
this.test.assert(this.cell_element_exists(string_index,
'.widget-area .widget-subarea div span.MathJax_Preview'),

View File

@ -34,11 +34,13 @@ from IPython.utils.py3compat import string_types
@contextmanager
def PropertyLock(instance, key, value):
instance._property_lock = (key, value)
yield
del instance._property_lock
try:
yield
finally:
del instance._property_lock
def should_send_property(instance, key, value):
return not hasattr(instance, _property_lock) or \
return not hasattr(instance, '_property_lock') or \
key != instance._property_lock[0] or \
value != instance._property_lock[1]
@ -47,8 +49,9 @@ class Widget(LoggingConfigurable):
# Shared declarations (Class level)
widget_construction_callback = None
widgets = []
keys = ['view_name']
keys = ['view_name'] # TODO: Sync = True
def on_widget_constructed(callback):
"""Class method, registers a callback to be called when a widget is
@ -66,6 +69,7 @@ class Widget(LoggingConfigurable):
# Public declarations (Instance level)
target_name = Unicode('widget', help="""Name of the backbone model
registered in the frontend to create and sync this widget with.""")
# model_name
view_name = Unicode(help="""Default view registered in the frontend
to use to represent the widget.""")
@ -75,23 +79,27 @@ class Widget(LoggingConfigurable):
def __init__(self, **kwargs):
"""Public constructor
"""
self.closed = False
self._display_callbacks = []
self._msg_callbacks = []
super(Widget, self).__init__(**kwargs)
self.on_trait_change(self._handle_property_changed, self.keys)
Widget.widgets.append(self)
Widget._call_widget_constructed(self)
def __del__(self):
"""Object disposal"""
self.close()
def close(self):
"""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 frontend."""
self._close_communication()
if not self.closed:
self.closed = True
self._close_communication()
Widget.widgets.remove(self)
@property
def comm(self):
@ -109,6 +117,8 @@ class Widget(LoggingConfigurable):
data = msg['content']['data']
method = data['method']
# TODO: Log unrecog.
# Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
if method == 'backbone' and 'sync_data' in data:
sync_data = data['sync_data']
@ -124,7 +134,7 @@ class Widget(LoggingConfigurable):
"""Called when a state is recieved from the frontend."""
for name in self.keys:
if name in sync_data:
value = sync_data[name]
value = self._unpack_widgets(sync_data[name])
with PropertyLock(self, name, value):
setattr(self, name, value)
@ -204,22 +214,57 @@ class Widget(LoggingConfigurable):
else:
keys = [key]
for k in keys:
value = getattr(self, k)
# a more elegant solution to encoding Widgets would be
# to tap into the JSON encoder and teach it how to deal
# with Widget objects, or maybe just teach the JSON
# encoder to look for a _repr_json property before giving
# up encoding
if isinstance(value, Widget):
value = value.model_id
elif isinstance(value, list) and len(value)>0 and isinstance(value[0], Widget):
# assume all elements of the list are widgets
value = [i.model_id for i in value]
state[k] = value
state[k] = self._pack_widgets(getattr(self, k))
return state
def _pack_widgets(self, values):
"""This function recursively converts all widget instances to model id
strings.
Children widgets will be stored and transmitted to the front-end by
their model ids."""
if isinstance(values, dict):
new_dict = {}
for key in values.keys():
new_dict[key] = self._pack_widgets(values[key])
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):
"""This function recursively converts all model id strings to widget
instances.
Children widgets will be stored and transmitted to the front-end by
their model ids."""
if isinstance(values, dict):
new_dict = {}
for key in values.keys():
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):
for widget in Widget.widgets:
if widget.model_id == values:
return widget
return values
else:
return values
def send(self, content):
"""Sends a custom msg to the widget model in the front-end.
@ -232,8 +277,9 @@ class Widget(LoggingConfigurable):
"custom_content": content})
def on_msg(self, callback, remove=False):
"""Register a callback for when a custom msg is recieved from the front-end
def on_msg(self, callback, remove=False): # TODO: Use lambdas and inspect here
"""Register or unregister a callback for when a custom msg is recieved
from the front-end.
Parameters
----------
@ -250,7 +296,8 @@ class Widget(LoggingConfigurable):
def on_displayed(self, callback, remove=False):
"""Register a callback to be called when the widget has been displayed
"""Register or unregister a callback to be called when the widget has
been displayed.
Parameters
----------
@ -282,10 +329,9 @@ class Widget(LoggingConfigurable):
def _open_communication(self):
"""Opens a communication with the front-end."""
# Create a comm.
if self._comm is None:
self._comm = Comm(target_name=self.target_name)
self._comm.on_msg(self._handle_msg)
self._comm.on_close(self._close_communication)
self._comm = Comm(target_name=self.target_name)
self._comm.on_msg(self._handle_msg)
self._comm.on_close(self._close_communication)
# first update
self.send_state()
@ -295,7 +341,7 @@ class Widget(LoggingConfigurable):
"""Closes a communication with the front-end."""
if self._comm is not None:
try:
self._comm.close()
self._comm.close() # TODO: Check
finally:
self._comm = None
@ -361,7 +407,7 @@ class DOMWidget(Widget):
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 in self._css[selector][key]):
if not (key in self._css[selector] and value == self._css[selector][key]):
self._css[selector][key] = value
self.send_state('_css')
else:
@ -379,7 +425,7 @@ class DOMWidget(Widget):
# Only update the property if it has changed.
key = args[0]
value = args[1]
if not (key in self._css[selector] and value in self._css[selector][key]):
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.
else:
@ -398,7 +444,7 @@ class DOMWidget(Widget):
be added to.
"""
class_list = class_names
if isinstance(list, class_list):
if isinstance(class_list, list):
class_list = ' '.join(class_list)
self.send({"msg_type": "add_class",
@ -418,7 +464,7 @@ class DOMWidget(Widget):
be removed from.
"""
class_list = class_names
if isinstance(list, class_list):
if isinstance(class_list, list):
class_list = ' '.join(class_list)
self.send({"msg_type": "remove_class",