class CommSocket(object): """ Manages the Comm connection between IPython and the browser (client). Comms are 2 way, with the CommSocket being able to publish a message via the send_json method, and handle a message with on_message. On the JS side figure.send_message and figure.ws.onmessage do the sending and receiving respectively. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) display(HTML("<div id=%r></div>" % self.uuid)) try: self.comm = Comm('matplotlib', data={'id': self.uuid}) except AttributeError: raise RuntimeError('Unable to create an IPython notebook Comm ' 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. if self.comm in self.manager.web_sockets: self.manager.remove_web_socket(self) self.comm.close() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. data_uri = "data:image/png;base64,{0}".format(b64encode(blob)) self.comm.send({'data': data_uri}) def on_message(self, message): # The 'supports_binary' message is relevant to the # websocket itself. The other messages get passed along # to matplotlib as-is. # Every message has a "type" and a "figure_id". message = json.loads(message['content']['data']) if message['type'] == 'closing': self.on_close() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: self.manager.handle_json(message)
class CommSocket(object): """ Manages the Comm connection between IPython and the browser (client). Comms are 2 way, with the CommSocket being able to publish a message via the send_json method, and handle a message with on_message. On the JS side figure.send_message and figure.ws.onmessage do the sending and receiving respectively. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) display(HTML("<div id=%r></div>" % self.uuid)) try: self.comm = Comm('matplotlib', data={'id': self.uuid}) except AttributeError: raise RuntimeError('Unable to create an IPython notebook Comm ' 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. if self.comm in self.manager.web_sockets: self.manager.remove_web_socket(self) self.comm.close() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. data_uri = "data:image/png;base64,{0}".format(b64encode(blob)) self.comm.send({'data': data_uri}) def on_message(self, message): # The 'supports_binary' message is relevant to the # websocket itself. The other messages get passed along # to matplotlib as-is. # Every message has a "type" and a "figure_id". message = json.loads(message['content']['data']) if message['type'] == 'closing': self.on_close() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: self.manager.handle_json(message)
class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- _widget_construction_callback = None widgets = {} widget_types = {} @staticmethod def on_widget_constructed(callback): """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 @staticmethod def _call_widget_constructed(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) @staticmethod def handle_comm_opened(comm, msg): """Static method, called when a widget is constructed.""" widget_class = import_item(msg['content']['data']['widget_class']) widget = widget_class(comm=comm) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- _model_module = Unicode(None, allow_none=True, help="""A requirejs module name in which to find _model_name. If empty, look in the global registry.""") _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") _view_module = Unicode(help="""A requirejs module in which to find _view_name. If empty, look in the global registry.""", sync=True) _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end to use to represent the widget.""", sync=True) comm = Instance('IPython.kernel.comm.Comm') msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the front-end can send before receiving an idle msg from the back-end.""") version = Int(0, sync=True, help="""Widget's version""") keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) _send_state_lock = Int(0) _states_to_send = Set(allow_none=False) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) #------------------------------------------------------------------------- # (Con/de)structor #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" self._model_id = kwargs.pop('model_id', None) super(Widget, self).__init__(**kwargs) Widget._call_widget_constructed(self) self.open() def __del__(self): """Object disposal""" self.close() #------------------------------------------------------------------------- # Properties #------------------------------------------------------------------------- def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: args = dict(target_name='ipython.widget', data={'model_name': self._model_name, 'model_module': self._model_module}) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) def _comm_changed(self, name, new): """Called when the comm is changed.""" if new is None: return self._model_id = self.model_id self.comm.on_msg(self._handle_msg) Widget.widgets[self.model_id] = self # first update self.send_state() @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" return self.comm.comm_id #------------------------------------------------------------------------- # Methods #------------------------------------------------------------------------- def close(self): """Close method. Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if self.comm is not None: Widget.widgets.pop(self.model_id, None) self.comm.close() self.comm = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- key : unicode, or iterable (optional) A single property's name or iterable of property names to sync with the front-end. """ self._send({ "method" : "update", "state" : self.get_state(key=key) }) def get_state(self, key=None): """Gets the widget state, or a piece of it. Parameters ---------- key : unicode or iterable (optional) A single property's name or iterable of property names to get. """ if key is None: keys = self.keys elif isinstance(key, string_types): keys = [key] elif isinstance(key, collections.Iterable): keys = key else: raise ValueError("key must be a string, an iterable of keys, or None") state = {} for k in keys: f = self.trait_metadata(k, 'to_json', self._trait_to_json) value = getattr(self, k) state[k] = f(value) return state def set_state(self, sync_data): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: json_value = sync_data[name] from_json = self.trait_metadata(name, 'from_json', self._trait_from_json) with self._lock_property(name, json_value): setattr(self, name, from_json(json_value)) 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. """ self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- 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) def on_displayed(self, callback, remove=False): """(Un)Register a widget displayed callback. Parameters ---------- callback: method handler 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) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, key, value): """Lock a property-value pair. The value should be the JSON state of the property. 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) @contextmanager def hold_sync(self): """Hold syncing any state until the context manager is released""" # We increment a value so that this can be nested. Syncing will happen when # all levels have been released. self._send_state_lock += 1 try: yield finally: self._send_state_lock -=1 if self._send_state_lock == 0: self.send_state(self._states_to_send) self._states_to_send.clear() def _should_send_property(self, key, value): """Check the property lock (property_lock)""" to_json = self.trait_metadata(key, 'to_json', self._trait_to_json) if (key == self._property_lock[0] and to_json(value) == self._property_lock[1]): return False elif self._send_state_lock > 0: self._states_to_send.add(key) return False else: return True # Event handlers @_show_traceback def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg['content']['data'] method = data['method'] # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. if method == 'backbone': if 'sync_data' in data: sync_data = data['sync_data'] self.set_state(sync_data) # handles all methods # Handle a state request. elif method == 'request_state': self.send_state() # Handle a custom msg from the front-end. elif method == 'custom': if 'content' in data: self._handle_custom_msg(data['content']) # Catch remainder. else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) def _notify_trait(self, name, old_value, new_value): """Called when a property has been changed.""" # Trigger default traitlet callback machinery. This allows any user # registered validation to be processed prior to allowing the widget # machinery to handle the state. LoggingConfigurable._notify_trait(self, name, old_value, new_value) # Send the state after the user registered callbacks for trait changes # have all fired (allows for user to validate values). if self.comm is not None and name in self.keys: # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new_value): # 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""" self._display_callbacks(self, **kwargs) def _trait_to_json(self, x): """Convert a trait value to json Traverse lists/tuples and dicts and serialize their values as well. Replace any widgets with their model_id """ if isinstance(x, dict): return {k: self._trait_to_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_to_json(v) for v in x] elif isinstance(x, Widget): return "IPY_MODEL_" + x.model_id else: return x # Value must be JSON-able def _trait_from_json(self, x): """Convert json values to objects Replace any strings representing valid model id values to Widget references. """ if isinstance(x, dict): return {k: self._trait_from_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_from_json(v) for v in x] elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: # we want to support having child widgets at any level in a hierarchy # trusting that a widget UUID will not appear out in the wild return Widget.widgets[x[10:]] else: return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. if self._view_name is not None: self._send({"method": "display"}) self._handle_displayed(**kwargs) def _send(self, msg): """Sends a message to the model in the front-end.""" self.comm.send(msg)
class BrowserContext(object): """Represents an in-browser context.""" def __init__(self): """Constructor""" self._calls = 0 self._callbacks = {} # Push the Javascript to the front-end. with open(os.path.join(os.path.split(__file__)[0], 'backend_context.js'), 'r') as f: display(Javascript(data=f.read())) # Open communication with the front-end. self._comm = Comm(target_name='BrowserContext') self._comm.on_msg(self._on_msg) def _on_msg(self, msg): """Handle messages from the front-end""" data = msg['content']['data'] # If the message is a call invoke, run the function and send # the results. if 'callback' in data: guid = data['callback'] callback = callback_registry[guid] args = data['arguments'] args = [self.deserialize(a) for a in args] index = data['index'] results = callback(*args) return self.serialize(self._send('return', index=index, results=results)) # The message is not a call invoke, it must be an object # that is a response to a Python request. else: index = data['index'] immutable = data['immutable'] value = data['value'] if index in self._callbacks: self._callbacks[index].resolve({ 'immutable': immutable, 'value': value }) del self._callbacks[index] def serialize(self, obj): """Serialize an object for sending to the front-end.""" if hasattr(obj, '_jsid'): return {'immutable': False, 'value': obj._jsid} else: obj_json = {'immutable': True} try: json.dumps(obj) obj_json['value'] = obj except: pass if callable(obj): guid = str(uuid.uuid4()) callback_registry[guid] = obj obj_json['callback'] = guid return obj_json def deserialize(self, obj): """Deserialize an object from the front-end.""" if obj['immutable']: return obj['value'] else: guid = obj['value'] if not guid in object_registry: instance = JSObject(self, guid) object_registry[guid] = instance return object_registry[guid] # Message types def getattr(self, parent, child): return self._send('getattr', parent=parent, child=child) def setattr(self, parent, child, value): return self._send('setattr', parent=parent, child=child, value=value) def apply(self, parent, function, *pargs): return self._send('apply', parent=parent, function=function, args=pargs) def _send(self, method, **parameters): """Sends a message to the front-end and returns a promise.""" msg = { 'index': self._calls, 'method': method, } msg.update(parameters) promise = SimplePromise() self._callbacks[self._calls] = promise self._calls += 1 self._comm.send(msg) return promise
class CommSocket(object): """ Manages the Comm connection between IPython and the browser (client). Comms are 2 way, with the CommSocket being able to publish a message via the send_json method, and handle a message with on_message. On the JS side figure.send_message and figure.ws.onmessage do the sending and receiving respectively. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) # Publish an output area with a unique ID. The javascript can then # hook into this area. display(HTML("<div id=%r></div>" % self.uuid)) try: self.comm = Comm("matplotlib", data={"id": self.uuid}) except AttributeError: raise RuntimeError( "Unable to create an IPython notebook Comm " "instance. Are you in the IPython notebook?" ) self.comm.on_msg(self.on_message) manager = self.manager self.comm.on_close(lambda close_message: manager.clearup_closed()) def is_open(self): return not self.comm._closed def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. self.comm.close() self.manager.clearup_closed() def send_json(self, content): self.comm.send({"data": json.dumps(content)}) def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. data = b64encode(blob) if six.PY3: data = data.decode("ascii") data_uri = "data:image/png;base64,{0}".format(data) self.comm.send({"data": data_uri}) def on_message(self, message): # The 'supports_binary' message is relevant to the # websocket itself. The other messages get passed along # to matplotlib as-is. # Every message has a "type" and a "figure_id". message = json.loads(message["content"]["data"]) if message["type"] == "closing": self.on_close() elif message["type"] == "supports_binary": self.supports_binary = message["value"] else: self.manager.handle_json(message)
class Canvas(object): def __init__(self): self.uuid = "canvas" + str(uuid4()) self.css = { "width": "500px", "height": "500px", "background-color": "white" } self.loop_func = None self.interval = 0.1 self.time = 0 self.stop = False self.stack = [] def begin(self): def func(): self.loop_func(self.time) self.send() self.time += 1 if not self.stop: timer = Timer(self.interval, func) timer.start() func() def show(self): from jinja2 import Template from IPython.core.display import display, HTML from IPython.kernel.comm import Comm path = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates/template.html")) template = Template(open(path).read()) css = str(self.css).replace("{", "").replace("}", "").replace(",", ";").replace("\'", "") html = template.render(**{ "canvas_id": self.uuid, "svg_style": css }) display(HTML(html)) self.comm = Comm("Draw") if self.loop_func is not None: self.begin() else: self.send() def add(self, obj): import re r = re.compile(r"__.*__") for attr in dir(obj): m = getattr(obj, str(attr)) if callable(m) and not r.match(str(attr)): def make_func(): m_ = m def func(*args, **kw): self.stack.append(("update", obj)) return m_(*args, **kw) return func setattr(obj, str(attr), make_func()) self.stack.append(("append", obj)) def send(self): self.comm.send({ "canvas_id": self.uuid, "queue": [{ "method": s[0], "type": s[1].__class__.__name__.lower(), "attr": s[1].attr, "selector_id": s[1].uuid } for s in self.stack] }) self.stack = [] def loop(self, func): self.loop_func = func def width(self, val): self.css["width"] = str(val) + "px" def height(self, val): self.css["height"] = str(val) + "px" def background_color(self, val): self.css["background-color"] = val
class Canvas(object): def __init__(self): self.uuid = "canvas" + str(uuid4()) self.css = { "width": "500px", "height": "500px", "background-color": "white" } self.loop_func = None self.interval = 0.1 self.time = 0 self.stop = False self.stack = [] def begin(self): def func(): self.loop_func(self.time) self.send() self.time += 1 if not self.stop: timer = Timer(self.interval, func) timer.start() func() def show(self): from jinja2 import Template from IPython.core.display import display, HTML from IPython.kernel.comm import Comm path = os.path.abspath( os.path.join(os.path.dirname(__file__), "templates/template.html")) template = Template(open(path).read()) css = str(self.css).replace("{", "").replace("}", "").replace( ",", ";").replace("\'", "") html = template.render(**{"canvas_id": self.uuid, "svg_style": css}) display(HTML(html)) self.comm = Comm("Draw") if self.loop_func is not None: self.begin() else: self.send() def add(self, obj): import re r = re.compile(r"__.*__") for attr in dir(obj): m = getattr(obj, str(attr)) if callable(m) and not r.match(str(attr)): def make_func(): m_ = m def func(*args, **kw): self.stack.append(("update", obj)) return m_(*args, **kw) return func setattr(obj, str(attr), make_func()) self.stack.append(("append", obj)) def send(self): self.comm.send({ "canvas_id": self.uuid, "queue": [{ "method": s[0], "type": s[1].__class__.__name__.lower(), "attr": s[1].attr, "selector_id": s[1].uuid } for s in self.stack] }) self.stack = [] def loop(self, func): self.loop_func = func def width(self, val): self.css["width"] = str(val) + "px" def height(self, val): self.css["height"] = str(val) + "px" def background_color(self, val): self.css["background-color"] = val