diff --git a/IPython/core/display.py b/IPython/core/display.py index d5641d3..cd82512 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -21,15 +21,10 @@ from __future__ import print_function import os -from .displaypub import ( - publish_pretty, publish_html, - publish_latex, publish_svg, - publish_png, publish_json, - publish_javascript, publish_jpeg -) - from IPython.utils.py3compat import string_types +from .displaypub import publish_display_data + #----------------------------------------------------------------------------- # utility functions #----------------------------------------------------------------------------- @@ -41,6 +36,41 @@ def _safe_exists(path): except Exception: return False +def _merge(d1, d2): + """Like update, but merges sub-dicts instead of clobbering at the top level. + + Updates d1 in-place + """ + + if not isinstance(d2, dict) or not isinstance(d1, dict): + return d2 + for key, value in d2.items(): + d1[key] = _merge(d1.get(key), value) + return d1 + +def _display_mimetype(mimetype, objs, raw=False, metadata=None): + """internal implementation of all display_foo methods + + Parameters + ---------- + mimetype : str + The mimetype to be published (e.g. 'image/png') + objs : tuple of objects + The Python objects to display, or if raw=True raw text data to + display. + raw : bool + Are the data objects raw data or Python objects that need to be + formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. + """ + if metadata: + metadata = {mimetype: metadata} + if raw: + # turn list of pngdata into list of { 'image/png': pngdata } + objs = [ {mimetype: obj} for obj in objs ] + display(*objs, raw=raw, metadata=metadata, include=[mimetype]) + #----------------------------------------------------------------------------- # Main functions #----------------------------------------------------------------------------- @@ -55,6 +85,9 @@ def display(*objs, **kwargs): ---------- objs : tuple of objects The Python objects to display. + raw : bool, optional + Are the objects to be displayed already mimetype-keyed dicts of raw display data, + or Python objects that need to be formatted before display? [default: False] include : list or tuple, optional A list of format type strings (MIME types) to include in the format data dict. If this is set *only* the format types included @@ -63,18 +96,29 @@ def display(*objs, **kwargs): A list of format type strings (MIME types) to exclude in the format data dict. If this is set all format types will be computed, except for those included in this argument. + metadata : dict, optional + A dictionary of metadata to associate with the output. + mime-type keys in this dictionary will be associated with the individual + representation formats, if they exist. """ + raw = kwargs.get('raw', False) include = kwargs.get('include') exclude = kwargs.get('exclude') + metadata = kwargs.get('metadata') from IPython.core.interactiveshell import InteractiveShell - inst = InteractiveShell.instance() - format = inst.display_formatter.format - publish = inst.display_pub.publish - - for obj in objs: - format_dict = format(obj, include=include, exclude=exclude) - publish('IPython.core.display.display', format_dict) + + if raw: + for obj in objs: + publish_display_data('display', obj, metadata) + else: + format = InteractiveShell.instance().display_formatter.format + for obj in objs: + format_dict, md_dict = format(obj, include=include, exclude=exclude) + if metadata: + # kwarg-specified metadata gets precedence + _merge(md_dict, metadata) + publish_display_data('display', format_dict, md_dict) def display_pretty(*objs, **kwargs): @@ -88,13 +132,10 @@ def display_pretty(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_pretty(obj) - else: - display(*objs, include=['text/plain']) + _display_mimetype('text/plain', objs, **kwargs) def display_html(*objs, **kwargs): @@ -108,13 +149,10 @@ def display_html(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_html(obj) - else: - display(*objs, include=['text/plain','text/html']) + _display_mimetype('text/html', objs, **kwargs) def display_svg(*objs, **kwargs): @@ -128,13 +166,10 @@ def display_svg(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_svg(obj) - else: - display(*objs, include=['text/plain','image/svg+xml']) + _display_mimetype('image/svg+xml', objs, **kwargs) def display_png(*objs, **kwargs): @@ -148,13 +183,10 @@ def display_png(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_png(obj) - else: - display(*objs, include=['text/plain','image/png']) + _display_mimetype('image/png', objs, **kwargs) def display_jpeg(*objs, **kwargs): @@ -168,13 +200,10 @@ def display_jpeg(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_jpeg(obj) - else: - display(*objs, include=['text/plain','image/jpeg']) + _display_mimetype('image/jpeg', objs, **kwargs) def display_latex(*objs, **kwargs): @@ -188,13 +217,10 @@ def display_latex(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_latex(obj) - else: - display(*objs, include=['text/plain','text/latex']) + _display_mimetype('text/latex', objs, **kwargs) def display_json(*objs, **kwargs): @@ -210,13 +236,10 @@ def display_json(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_json(obj) - else: - display(*objs, include=['text/plain','application/json']) + _display_mimetype('application/json', objs, **kwargs) def display_javascript(*objs, **kwargs): @@ -230,13 +253,10 @@ def display_javascript(*objs, **kwargs): raw : bool Are the data objects raw data or Python objects that need to be formatted before display? [default: False] + metadata : dict (optional) + Metadata to be associated with the specific mimetype output. """ - raw = kwargs.pop('raw',False) - if raw: - for obj in objs: - publish_javascript(obj) - else: - display(*objs, include=['text/plain','application/javascript']) + _display_mimetype('application/javascript', objs, **kwargs) #----------------------------------------------------------------------------- # Smart classes @@ -539,14 +559,26 @@ class Image(DisplayObject): if self.height: height = ' height="%d"' % self.height return u'' % (self.url, width, height) + + def _data_and_metadata(self): + """shortcut for returning metadata with shape information, if defined""" + md = {} + if self.width: + md['width'] = self.width + if self.height: + md['height'] = self.height + if md: + return self.data, md + else: + return self.data def _repr_png_(self): if self.embed and self.format == u'png': - return self.data + return self._data_and_metadata() def _repr_jpeg_(self): if self.embed and (self.format == u'jpeg' or self.format == u'jpg'): - return self.data + return self._data_and_metadata() def _find_ext(self, s): return unicode(s.split('.')[-1].lower()) diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 6cd5568..1d9ffde 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -145,15 +145,18 @@ class DisplayHook(Configurable): Returns ------- - format_data : dict - A :class:`dict` whose keys are valid MIME types and values are + (format_dict, md_dict) : dict + format_dict is a :class:`dict` whose keys are valid MIME types and values are JSON'able raw data for that MIME type. It is recommended that all return values of this should always include the "text/plain" MIME type representation of the object. + md_dict is a :class:`dict` with the same MIME type keys + of metadata associated with each output. + """ return self.shell.display_formatter.format(result) - def write_format_data(self, format_dict): + def write_format_data(self, format_dict, md_dict=None): """Write the format data dict to the frontend. This default version of this method simply writes the plain text @@ -165,6 +168,8 @@ class DisplayHook(Configurable): ---------- format_dict : dict The format dict for the object passed to `sys.displayhook`. + md_dict : dict (optional) + The metadata dict to be associated with the display data. """ # We want to print because we want to always make sure we have a # newline, even if all the prompt separators are ''. This is the @@ -239,8 +244,8 @@ class DisplayHook(Configurable): if result is not None and not self.quiet(): self.start_displayhook() self.write_output_prompt() - format_dict = self.compute_format_data(result) - self.write_format_data(format_dict) + format_dict, md_dict = self.compute_format_data(result) + self.write_format_data(format_dict, md_dict) self.update_user_ns(result) self.log_output(format_dict) self.finish_displayhook() diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py index 997532b..c12d873 100644 --- a/IPython/core/displaypub.py +++ b/IPython/core/displaypub.py @@ -3,7 +3,7 @@ There are two components of the display system: * Display formatters, which take a Python object and compute the - representation of the object in various formats (text, HTML, SVg, etc.). + representation of the object in various formats (text, HTML, SVG, etc.). * The display publisher that is used to send the representation data to the various frontends. @@ -98,7 +98,9 @@ class DisplayPublisher(Configurable): 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. + the data. Metadata specific to each mime-type can be specified + in the metadata dict with the same mime-type keys as + the data itself. """ # The default is to simply write the plain text data using io.stdout. @@ -149,8 +151,9 @@ def publish_display_data(source, data, metadata=None): 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. - """ + the data. mime-type keys matching those in data can be used + to specify metadata about particular representations. + """ from IPython.core.interactiveshell import InteractiveShell InteractiveShell.instance().display_pub.publish( source, @@ -159,151 +162,3 @@ def publish_display_data(source, data, metadata=None): ) -def publish_pretty(data, metadata=None): - """Publish raw text data to all frontends. - - Parameters - ---------- - data : unicode - The raw text data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_pretty', - {'text/plain':data}, - metadata=metadata - ) - - -def publish_html(data, metadata=None): - """Publish raw HTML data to all frontends. - - Parameters - ---------- - data : unicode - The raw HTML data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_html', - {'text/html':data}, - metadata=metadata - ) - - -def publish_latex(data, metadata=None): - """Publish raw LaTeX data to all frontends. - - Parameters - ---------- - data : unicode - The raw LaTeX data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_latex', - {'text/latex':data}, - metadata=metadata - ) - -def publish_png(data, metadata=None): - """Publish raw binary PNG data to all frontends. - - Parameters - ---------- - data : str/bytes - The raw binary PNG data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_png', - {'image/png':data}, - metadata=metadata - ) - - -def publish_jpeg(data, metadata=None): - """Publish raw binary JPEG data to all frontends. - - Parameters - ---------- - data : str/bytes - The raw binary JPEG data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_jpeg', - {'image/jpeg':data}, - metadata=metadata - ) - - -def publish_svg(data, metadata=None): - """Publish raw SVG data to all frontends. - - Parameters - ---------- - data : unicode - The raw SVG data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_svg', - {'image/svg+xml':data}, - metadata=metadata - ) - -def publish_json(data, metadata=None): - """Publish raw JSON data to all frontends. - - Parameters - ---------- - data : unicode - The raw JSON data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_json', - {'application/json':data}, - metadata=metadata - ) - -def publish_javascript(data, metadata=None): - """Publish raw Javascript data to all frontends. - - Parameters - ---------- - data : unicode - The raw Javascript data to publish. - 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. - """ - publish_display_data( - u'IPython.core.displaypub.publish_javascript', - {'application/javascript':data}, - metadata=metadata - ) - diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index 0f89332..895e6aa 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -127,28 +127,43 @@ class DisplayFormatter(Configurable): Returns ------- - format_dict : dict - A dictionary of key/value pairs, one or each format that was + (format_dict, metadata_dict) : tuple of two dicts + + format_dict is a dictionary of key/value pairs, one of each format that was generated for the object. The keys are the format types, which will usually be MIME type strings and the values and JSON'able data structure containing the raw data for the representation in that format. + + metadata_dict is a dictionary of metadata about each mime-type output. + Its keys will be a strict subset of the keys in format_dict. """ format_dict = {} + md_dict = {} for format_type, formatter in self.formatters.items(): if include and format_type not in include: continue if exclude and format_type in exclude: continue + + md = None try: data = formatter(obj) except: # FIXME: log the exception raise + + # formatters can return raw data or (data, metadata) + if isinstance(data, tuple) and len(data) == 2: + data, md = data + if data is not None: format_dict[format_type] = data - return format_dict + if md is not None: + md_dict[format_type] = md + + return format_dict, md_dict @property def format_types(self): diff --git a/IPython/display.py b/IPython/display.py index a887127..7d248ba 100644 --- a/IPython/display.py +++ b/IPython/display.py @@ -13,5 +13,4 @@ #----------------------------------------------------------------------------- from IPython.core.display import * -from IPython.core.displaypub import * from IPython.lib.display import * diff --git a/IPython/frontend/html/notebook/static/js/outputarea.js b/IPython/frontend/html/notebook/static/js/outputarea.js index 4329391..a57ef56 100644 --- a/IPython/frontend/html/notebook/static/js/outputarea.js +++ b/IPython/frontend/html/notebook/static/js/outputarea.js @@ -178,9 +178,11 @@ var IPython = (function (IPython) { json.stream = content.name; } else if (msg_type === "display_data") { json = this.convert_mime_types(json, content.data); + json.metadata = this.convert_mime_types({}, content.metadata); } else if (msg_type === "pyout") { json.prompt_number = content.execution_count; json = this.convert_mime_types(json, content.data); + json.metadata = this.convert_mime_types({}, content.metadata); } else if (msg_type === "pyerr") { json.ename = content.ename; json.evalue = content.evalue; @@ -273,7 +275,7 @@ var IPython = (function (IPython) { } s = s + '\n'; var toinsert = this.create_output_area(); - this.append_text(s, toinsert); + this.append_text(s, {}, toinsert); this.element.append(toinsert); } }; @@ -310,7 +312,7 @@ var IPython = (function (IPython) { // If we got here, attach a new div var toinsert = this.create_output_area(); - this.append_text(text, toinsert, "output_stream "+subclass); + this.append_text(text, {}, toinsert, "output_stream "+subclass); this.element.append(toinsert); }; @@ -331,12 +333,16 @@ var IPython = (function (IPython) { for(var type_i in OutputArea.display_order){ var type = OutputArea.display_order[type_i]; if(json[type] != undefined ){ + var md = {}; + if (json.metadata && json.metadata[type]) { + md = json.metadata[type]; + }; if(type == 'javascript'){ if (dynamic) { - this.append_javascript(json.javascript, element, dynamic); + this.append_javascript(json.javascript, md, element, dynamic); } } else { - this['append_'+type](json[type], element); + this['append_'+type](json[type], md, element); } return; } @@ -344,14 +350,14 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_html = function (html, element) { + OutputArea.prototype.append_html = function (html, md, element) { var toinsert = $("
").addClass("output_subarea output_html rendered_html"); toinsert.append(html); element.append(toinsert); }; - OutputArea.prototype.append_javascript = function (js, container) { + OutputArea.prototype.append_javascript = function (js, md, container) { // We just eval the JS code, element appears in the local scope. var element = $("
").addClass("output_subarea"); container.append(element); @@ -375,7 +381,7 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_text = function (data, element, extra_class) { + OutputArea.prototype.append_text = function (data, md, element, extra_class) { var toinsert = $("
").addClass("output_subarea output_text"); // escape ANSI & HTML specials in plaintext: data = utils.fixConsole(data); @@ -389,7 +395,7 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_svg = function (svg, element) { + OutputArea.prototype.append_svg = function (svg, md, element) { var toinsert = $("
").addClass("output_subarea output_svg"); toinsert.append(svg); element.append(toinsert); @@ -423,25 +429,37 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_png = function (png, element) { + OutputArea.prototype.append_png = function (png, md, element) { var toinsert = $("
").addClass("output_subarea output_png"); var img = $("").attr('src','data:image/png;base64,'+png); + if (md['height']) { + img.attr('height', md['height']); + } + if (md['width']) { + img.attr('width', md['width']); + } this._dblclick_to_reset_size(img); toinsert.append(img); element.append(toinsert); }; - OutputArea.prototype.append_jpeg = function (jpeg, element) { + OutputArea.prototype.append_jpeg = function (jpeg, md, element) { var toinsert = $("
").addClass("output_subarea output_jpeg"); var img = $("").attr('src','data:image/jpeg;base64,'+jpeg); + if (md['height']) { + img.attr('height', md['height']); + } + if (md['width']) { + img.attr('width', md['width']); + } this._dblclick_to_reset_size(img); toinsert.append(img); element.append(toinsert); }; - OutputArea.prototype.append_latex = function (latex, element) { + OutputArea.prototype.append_latex = function (latex, md, element) { // This method cannot do the typesetting because the latex first has to // be on the page. var toinsert = $("
").addClass("output_subarea output_latex"); diff --git a/IPython/kernel/zmq/displayhook.py b/IPython/kernel/zmq/displayhook.py index bcfd01a..ecedbe7 100644 --- a/IPython/kernel/zmq/displayhook.py +++ b/IPython/kernel/zmq/displayhook.py @@ -52,8 +52,9 @@ class ZMQShellDisplayHook(DisplayHook): """Write the output prompt.""" self.msg['content']['execution_count'] = self.prompt_count - def write_format_data(self, format_dict): + def write_format_data(self, format_dict, md_dict=None): self.msg['content']['data'] = encode_images(format_dict) + self.msg['content']['metadata'] = md_dict def finish_displayhook(self): """Finish up all displayhook activities.""" diff --git a/IPython/nbformat/v3/nbbase.py b/IPython/nbformat/v3/nbbase.py index 4593f29..bac60df 100644 --- a/IPython/nbformat/v3/nbbase.py +++ b/IPython/nbformat/v3/nbbase.py @@ -53,12 +53,18 @@ def from_dict(d): def new_output(output_type=None, output_text=None, output_png=None, output_html=None, output_svg=None, output_latex=None, output_json=None, output_javascript=None, output_jpeg=None, prompt_number=None, - ename=None, evalue=None, traceback=None, stream=None): + ename=None, evalue=None, traceback=None, stream=None, metadata=None): """Create a new code cell with input and output""" output = NotebookNode() if output_type is not None: output.output_type = unicode(output_type) + if metadata is None: + metadata = {} + if not isinstance(metadata, dict): + raise TypeError("metadata must be dict") + output.metadata = metadata + if output_type != 'pyerr': if output_text is not None: output.text = unicode(output_text) @@ -91,7 +97,7 @@ def new_output(output_type=None, output_text=None, output_png=None, if output_type == u'stream': output.stream = 'stdout' if stream is None else unicode(stream) - + return output diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 5b150a5..e3088c2 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -808,8 +808,7 @@ Message type: ``display_data``:: # The data dict contains key/value pairs, where the kids are MIME # types and the values are the raw data of the representation in that - # format. The data dict must minimally contain the ``text/plain`` - # MIME type which is used as a backup representation. + # format. 'data' : dict, # Any metadata that describes the data @@ -817,6 +816,24 @@ Message type: ``display_data``:: } +The ``metadata`` contains any metadata that describes the output. +Global keys are assumed to apply to the output as a whole. +The ``metadata`` dict can also contain mime-type keys, which will be sub-dictionaries, +which are interpreted as applying only to output of that type. +Third parties should put any data they write into a single dict +with a reasonably unique name to avoid conflicts. + +The only metadata keys currently defined in IPython are the width and height +of images:: + + 'metadata' : { + 'image/png' : { + 'width': 640, + 'height': 480 + } + } + + Raw Data Publication --------------------