diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py index 375ddb5..f3bcf39 100644 --- a/IPython/core/displaypub.py +++ b/IPython/core/displaypub.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""An interface for publishing data related to the display of objects. +"""An interface for publishing rich data to frontends. Authors: @@ -35,6 +35,69 @@ class DisplayPublisher(Configurable): raise TypeError('metadata must be a dict, got: %r' % data) def publish(self, source, data, metadata=None): - """Publish data and metadata to all frontends.""" - pass + """Publish data and metadata to all frontends. + See the ``display_data`` message in the messaging documentation for + more details about this message type. + + Parameters + ---------- + source : str + A string that give the function or method that created the data, + such as 'IPython.core.page'. + data : dict + A dictionary having keys that are valid MIME types (like + 'text/plain' or 'image/svg+xml') and values that are the data for + that MIME type. The data itself must be a JSON'able data + structure. Minimally all data should have the 'text/plain' data, + which can be displayed by all frontends. If more than the plain + text is given, it is up to the frontend to decide which + representation to use. + metadata : dict + A dictionary for metadata related to the data. This can contain + arbitrary key, value pairs that frontends can use to interpret + the data. + """ + from IPython.utils import io + # The default is to simply write the plain text data using io.Term. + if data.has_key('text/plain'): + print >>io.Term.cout, data['text/plain'] + + +def publish_display_data(source, text, svg=None, png=None, + html=None, metadata=None): + """Publish a display data to the frontends. + + This function is a high level helper for the publishing of display data. + It handle a number of common MIME types in a clean API. For other MIME + types, use ``get_ipython().display_pub.publish`` directly. + + Parameters + ---------- + text : str/unicode + The string representation of the plot. + + svn : str/unicode + The raw svg data of the plot. + + png : ??? + The raw png data of the plot. + + metadata : dict, optional [default empty] + Allows for specification of additional information about the plot data. + """ + from IPython.core.interactiveshell import InteractiveShell + + data_dict = {} + data_dict['text/plain'] = text + if svg is not None: + data_dict['image/svg+xml'] = svg + if png is not None: + data_dict['image/png'] = png + if html is not None: + data_dict['text/html'] = html + InteractiveShell.instance().display_pub.publish( + source, + data_dict, + metadata + ) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 52753b3..a2e646f 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -41,6 +41,7 @@ from IPython.core.builtin_trap import BuiltinTrap from IPython.core.compilerop import CachingCompiler from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook +from IPython.core.displaypub import DisplayPublisher from IPython.core.error import TryNext, UsageError from IPython.core.extensions import ExtensionManager from IPython.core.fakemodule import FakeModule, init_fakemod_dict @@ -150,6 +151,8 @@ class InteractiveShell(Configurable, Magic): debug = CBool(False, config=True) deep_reload = CBool(False, config=True) displayhook_class = Type(DisplayHook) + display_pub_class = Type(DisplayPublisher) + exit_now = CBool(False) # Monotonically increasing execution counter execution_count = Int(1) @@ -284,6 +287,7 @@ class InteractiveShell(Configurable, Magic): self.init_io() self.init_traceback_handlers(custom_exceptions) self.init_prompts() + self.init_display_pub() self.init_displayhook() self.init_reload_doctest() self.init_magics() @@ -481,6 +485,9 @@ class InteractiveShell(Configurable, Magic): # will initialize that object and all prompt related information. pass + def init_display_pub(self): + self.display_pub = self.display_pub_class(config=self.config) + def init_displayhook(self): # Initialize displayhook, set in/out prompts and printing system self.displayhook = self.displayhook_class( diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index f879131..bc7db31 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -183,6 +183,21 @@ class IPythonWidget(FrontendWidget): self._append_html(self._make_out_prompt(prompt_number)) self._append_plain_text(content['data']+self.output_sep2) + def _handle_display_data(self, msg): + """ The base handler for the ``display_data`` message. + """ + # For now, we don't display data from other frontends, but we + # eventually will as this allows all frontends to monitor the display + # data. But we need to figure out how to handle this in the GUI. + if not self._hidden and self._is_from_this_session(msg): + source = msg['content']['source'] + data = msg['content']['data'] + metadata = msg['content']['metadata'] + # In the regular IPythonWidget, we simply print the plain text + # representation. + if data.has_key('text/plain'): + self._append_plain_text(data['text/plain']) + def _started_channels(self): """ Reimplemented to make a history request. """ @@ -444,7 +459,7 @@ class IPythonWidget(FrontendWidget): else: self._page(item['text'], html=False) - #------ Trait change handlers --------------------------------------------- + #------ Trait change handlers -------------------------------------------- def _style_sheet_changed(self): """ Set the style sheets of the underlying widgets. @@ -464,4 +479,4 @@ class IPythonWidget(FrontendWidget): self._highlighter.set_style(self.syntax_style) else: self._highlighter.set_style_sheet(self.style_sheet) - + diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index 98d0cb1..377e778 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -56,7 +56,31 @@ class RichIPythonWidget(IPythonWidget): menu.addAction('Save SVG As...', lambda: save_svg(svg, self._control)) return menu - + + #--------------------------------------------------------------------------- + # 'BaseFrontendMixin' abstract interface + #--------------------------------------------------------------------------- + + def _handle_display_data(self, msg): + """ A handler for ``display_data`` message that handles html and svg. + """ + if not self._hidden and self._is_from_this_session(msg): + source = msg['content']['source'] + data = msg['content']['data'] + metadata = msg['content']['metadata'] + # Try to use the svg or html representations. + # FIXME: Is this the right ordering of things to try? + if data.has_key('image/svg+xml'): + svg = data['image/svg+xml'] + # TODO: try/except this call. + self._append_svg(svg) + elif data.has_key('text/html'): + html = data['text/html'] + self._append_html(html) + else: + # Default back to the plain text representation. + return super(RichIPythonWidget, self)._handle_display_data(msg) + #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- @@ -65,20 +89,11 @@ class RichIPythonWidget(IPythonWidget): """ Reimplemented to handle matplotlib plot payloads. """ if item['source'] == self._payload_source_plot: + # TODO: remove this as all plot data is coming back through the + # display_data message type. if item['format'] == 'svg': svg = item['data'] - try: - image = svg_to_image(svg) - except ValueError: - self._append_plain_text('Received invalid plot data.') - else: - format = self._add_image(image) - self._name_to_svg[str(format.name())] = svg - format.setProperty(self._svg_text_format_property, svg) - cursor = self._get_end_cursor() - cursor.insertBlock() - cursor.insertImage(format) - cursor.insertBlock() + self._append_svg(svg) return True else: # Add other plot formats here! @@ -90,6 +105,22 @@ class RichIPythonWidget(IPythonWidget): # 'RichIPythonWidget' protected interface #--------------------------------------------------------------------------- + def _append_svg(self, svg): + """ Append raw svg data to the widget. + """ + try: + image = svg_to_image(svg) + except ValueError: + self._append_plain_text('Received invalid plot data.') + else: + format = self._add_image(image) + self._name_to_svg[str(format.name())] = svg + format.setProperty(self._svg_text_format_property, svg) + cursor = self._get_end_cursor() + cursor.insertBlock() + cursor.insertImage(format) + cursor.insertBlock() + def _add_image(self, image): """ Adds the specified QImage to the document and returns a QTextImageFormat that references it. @@ -192,4 +223,4 @@ class RichIPythonWidget(IPythonWidget): else: return 'Unrecognized image format' - + diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py index 98df284..79b8edb 100644 --- a/IPython/frontend/qt/kernelmanager.py +++ b/IPython/frontend/qt/kernelmanager.py @@ -101,6 +101,9 @@ class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel): # Emitted when a message of type 'pyerr' is received. pyerr_received = QtCore.pyqtSignal(object) + # Emitted when a message of type 'display_data' is received + display_data_received = QtCore.pyqtSignal(object) + # Emitted when a crash report message is received from the kernel's # last-resort sys.excepthook. crash_received = QtCore.pyqtSignal(object) @@ -117,7 +120,6 @@ class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel): """ # Emit the generic signal. self.message_received.emit(msg) - # Emit signals for specialized message types. msg_type = msg['msg_type'] signal = getattr(self, msg_type + '_received', None) diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 011608f..018515c 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -91,6 +91,8 @@ class Kernel(Configurable): self.shell = ZMQInteractiveShell.instance() self.shell.displayhook.session = self.session self.shell.displayhook.pub_socket = self.pub_socket + self.shell.display_pub.session = self.session + self.shell.display_pub.pub_socket = self.pub_socket # TMP - hack while developing self.shell._reply_content = None @@ -194,6 +196,7 @@ class Kernel(Configurable): # Set the parent message of the display hook and out streams. shell.displayhook.set_parent(parent) + shell.display_pub.set_parent(parent) sys.stdout.set_parent(parent) sys.stderr.set_parent(parent) diff --git a/IPython/zmq/pylab/backend_inline.py b/IPython/zmq/pylab/backend_inline.py index f844d4c..745dbf6 100644 --- a/IPython/zmq/pylab/backend_inline.py +++ b/IPython/zmq/pylab/backend_inline.py @@ -14,7 +14,7 @@ from matplotlib.backends.backend_svg import new_figure_manager from matplotlib._pylab_helpers import Gcf # Local imports. -from backend_payload import add_plot_payload +from IPython.core.displaypub import publish_display_data #----------------------------------------------------------------------------- # Functions @@ -85,7 +85,11 @@ def send_svg_canvas(canvas): canvas.figure.set_facecolor('white') canvas.figure.set_edgecolor('white') try: - add_plot_payload('svg', svg_from_canvas(canvas)) + publish_display_data( + 'IPython.zmq.pylab.backend_inline.send_svg_canvas', + '', + svg=svg_from_canvas(canvas) + ) finally: canvas.figure.set_facecolor(fc) canvas.figure.set_edgecolor(ec) diff --git a/IPython/zmq/pylab/backend_payload.py b/IPython/zmq/pylab/backend_payload.py deleted file mode 100644 index 74d4de6..0000000 --- a/IPython/zmq/pylab/backend_payload.py +++ /dev/null @@ -1,26 +0,0 @@ -""" Provides basic funtionality for payload backends. -""" - -# Local imports. -from IPython.core.interactiveshell import InteractiveShell - - -def add_plot_payload(format, data, metadata={}): - """ Add a plot payload to the current execution reply. - - Parameters: - ----------- - format : str - Identifies the format of the plot data. - - data : str - The raw plot data. - - metadata : dict, optional [default empty] - Allows for specification of additional information about the plot data. - """ - payload = dict( - source='IPython.zmq.pylab.backend_payload.add_plot_payload', - format=format, data=data, metadata=metadata - ) - InteractiveShell.instance().payload_manager.write_payload(payload) diff --git a/IPython/zmq/zmqshell.py b/IPython/zmq/zmqshell.py index 1eda3d9..66e5369 100644 --- a/IPython/zmq/zmqshell.py +++ b/IPython/zmq/zmqshell.py @@ -26,6 +26,7 @@ from IPython.core.interactiveshell import ( ) from IPython.core import page from IPython.core.displayhook import DisplayHook +from IPython.core.displaypub import DisplayPublisher from IPython.core.macro import Macro from IPython.core.payloadpage import install_payload_page from IPython.utils import io @@ -75,10 +76,34 @@ class ZMQDisplayHook(DisplayHook): self.msg = None +class ZMQDisplayPublisher(DisplayPublisher): + """A ``DisplayPublisher`` that published data using a ZeroMQ PUB socket.""" + + session = Instance(Session) + pub_socket = Instance('zmq.Socket') + parent_header = Dict({}) + + def set_parent(self, parent): + """Set the parent for outbound messages.""" + self.parent_header = extract_header(parent) + + def publish(self, source, data, metadata=None): + if metadata is None: + metadata = {} + self._validate_data(source, data, metadata) + msg = self.session.msg(u'display_data', {}, parent=self.parent_header) + msg['content']['source'] = source + msg['content']['data'] = data + msg['content']['metadata'] = metadata + self.pub_socket.send_json(msg) + + class ZMQInteractiveShell(InteractiveShell): """A subclass of InteractiveShell for ZMQ.""" displayhook_class = Type(ZMQDisplayHook) + display_pub_class = Type(ZMQDisplayPublisher) + keepkernel_on_exit = None def init_environment(self): diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 15cbf02..59fdbc6 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -700,30 +700,32 @@ socket with the names 'stdin' and 'stdin_reply'. This will allow other clients to monitor/display kernel interactions and possibly replay them to their user or otherwise expose them. -Representation Data -------------------- +Display Data +------------ -This type of message is used to bring back representations (text, html, svg, -etc.) of Python objects to the frontend. Each message can have multiple -representations of the object; it is up to the frontend to decide which to use -and how. A single message should contain the different representations of a -single Python object. Each representation should be a JSON'able data structure, -and should be a valid MIME type. +This type of message is used to bring back data that should be diplayed (text, +html, svg, etc.) in the frontends. This data is published to all frontends. +Each message can have multiple representations of the data; it is up to the +frontend to decide which to use and how. A single message should contain all +possible representations of the same information. Each representation should +be a JSON'able data structure, and should be a valid MIME type. Some questions remain about this design: -* Do we use this message type for pyout/displayhook? -* What is the best way to organize the content dict of the message? +* Do we use this message type for pyout/displayhook? Probably not, because + the displayhook also has to handle the Out prompt display. On the other hand + we could put that information into the metadata secion. -Message type: ``repr_data``:: +Message type: ``display_data``:: - # Option 1: if we only allow a single source. content = { 'source' : str # Who create the data 'data' : dict # {'mimetype1' : data1, 'mimetype2' : data2} 'metadata' : dict # Any metadata that describes the data } +Other options for ``display_data`` content:: + # Option 2: allowing for a different source for each representation, but not keyed by anything. content = {