diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index bb4d9d0..b2846de 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -26,7 +26,9 @@ from IPython.utils.py3compat import string_types #----------------------------------------------------------------------------- class Widget(LoggingConfigurable): - # Shared declarations (Class level) + #------------------------------------------------------------------------- + # Class attributes + #------------------------------------------------------------------------- widget_construction_callback = None widgets = {} @@ -42,42 +44,18 @@ class Widget(LoggingConfigurable): if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback): Widget.widget_construction_callback(widget) - # Public declarations (Instance level) + #------------------------------------------------------------------------- + # Traits + #------------------------------------------------------------------------- model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") view_name = Unicode(help="""Default view registered in the front-end to use to represent the widget.""", sync=True) - - @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] - - @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 - - # Private/protected declarations _comm = Instance('IPython.kernel.comm.Comm') - + + #------------------------------------------------------------------------- + # (Con/de)structor + #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" self.closed = False @@ -94,21 +72,17 @@ class Widget(LoggingConfigurable): """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 front-end.""" - if not self.closed: - self._comm.close() - self._close() - - def _close(self): - """Unsafe close""" - del Widget.widgets[self.model_id] - self._comm = None - self.closed = True + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + @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 @property def comm(self): @@ -127,50 +101,19 @@ class Widget(LoggingConfigurable): def model_id(self): return self.comm.comm_id - # 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': - if 'custom_content' in data: - self._handle_custom_msg(data['custom_content']) - - 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]) - with self.property_lock(name, value): - setattr(self, name, value) - - def _handle_custom_msg(self, content): - """Called when a custom msg is received.""" - for handler in self._msg_callbacks: - handler(self, content) - - 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) + #------------------------------------------------------------------------- + # Methods + #------------------------------------------------------------------------- + def close(self): + """Close method. - def _handle_displayed(self, **kwargs): - """Called when a view has been displayed for this widget instance""" - for handler in self._display_callbacks: - handler(self, **kwargs) + 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() - # Public methods def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. @@ -195,49 +138,6 @@ class Widget(LoggingConfigurable): keys = self.keys if key is None else [key] return {k: self._pack_widgets(getattr(self, k)) for k in keys} - def _pack_widgets(self, values): - """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, value in values.items(): - new_dict[key] = self._pack_widgets(value) - 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): - """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, values in values.items(): - 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): - if values in Widget.widgets: - return Widget.widgets[values] - else: - return values - else: - return values - def send(self, content): """Sends a custom msg to the widget model in the front-end. @@ -300,7 +200,119 @@ class Widget(LoggingConfigurable): else: raise Exception('Callback must be callable.') + #------------------------------------------------------------------------- # Support methods + #------------------------------------------------------------------------- + @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] + + 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': + if 'custom_content' in data: + self._handle_custom_msg(data['custom_content']) + + 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]) + with self._property_lock(name, value): + setattr(self, name, value) + + def _handle_custom_msg(self, content): + """Called when a custom msg is received.""" + for handler in self._msg_callbacks: + handler(self, content) + + 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""" + for handler in self._display_callbacks: + handler(self, **kwargs) + + def _pack_widgets(self, values): + """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, value in values.items(): + new_dict[key] = self._pack_widgets(value) + 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): + """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, values in values.items(): + 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): + if values in Widget.widgets: + return Widget.widgets[values] + else: + return values + else: + return values + def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. By sending a display message, the comm is opened and the @@ -315,8 +327,6 @@ class Widget(LoggingConfigurable): class DOMWidget(Widget): visible = Bool(True, help="Whether or not the widget is visible.", sync=True) - - # Private/protected declarations _css = Dict(sync=True) # Internal CSS property dict def get_css(self, key, selector=""):