diff --git a/IPython/core/display.py b/IPython/core/display.py index 8c7a1137f6b..85150eb620a 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -304,6 +304,11 @@ def __init__(self, data=None, url=None, filename=None): self.filename = None if filename is None else unicode_type(filename) self.reload() + self._check_data() + + def _check_data(self): + """Override in subclasses if there's something to check.""" + pass def reload(self): """Reload the raw data from file or URL.""" @@ -331,13 +336,19 @@ def reload(self): except: self.data = None -class Pretty(DisplayObject): +class TextDisplayObject(DisplayObject): + """Validate that display data is text""" + def _check_data(self): + if self.data is not None and not isinstance(self.data, string_types): + raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data)) + +class Pretty(TextDisplayObject): def _repr_pretty_(self): return self.data -class HTML(DisplayObject): +class HTML(TextDisplayObject): def _repr_html_(self): return self.data @@ -351,14 +362,14 @@ def __html__(self): return self._repr_html_() -class Math(DisplayObject): +class Math(TextDisplayObject): def _repr_latex_(self): s = self.data.strip('$') return "$$%s$$" % s -class Latex(DisplayObject): +class Latex(TextDisplayObject): def _repr_latex_(self): return self.data @@ -398,7 +409,7 @@ def _repr_svg_(self): return self.data -class JSON(DisplayObject): +class JSON(TextDisplayObject): def _repr_json_(self): return self.data @@ -415,7 +426,7 @@ def _repr_json_(self): lib_t2 = """}); """ -class Javascript(DisplayObject): +class Javascript(TextDisplayObject): def __init__(self, data=None, url=None, filename=None, lib=None, css=None): """Create a Javascript display object given raw data. diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index a5a51a4332e..b4ccf03f3fc 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -33,12 +33,13 @@ # Our own imports from IPython.config.configurable import Configurable from IPython.lib import pretty +from IPython.utils import io from IPython.utils.traitlets import ( Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List, ) from IPython.utils.warn import warn from IPython.utils.py3compat import ( - unicode_to_str, with_metaclass, PY3, string_types, + unicode_to_str, with_metaclass, PY3, string_types, unicode_type, ) if PY3: @@ -182,15 +183,22 @@ def format_types(self): # Formatters for specific format types (text, html, svg, etc.) #----------------------------------------------------------------------------- - @decorator def warn_format_error(method, self, *args, **kwargs): """decorator for warning on failed format call""" try: - return method(self, *args, **kwargs) + r = method(self, *args, **kwargs) except Exception as e: warn("Exception in %s formatter: %s" % (self.format_type, e)) return None + if r is None or isinstance(r, self._return_type) or \ + (isinstance(r, tuple) and r and isinstance(r[0], self._return_type)): + return r + else: + warn("%s formatter returned invalid type %s (expected %s) for object: %s" % ( + self.format_type, type(r), self._return_type, pretty._safe_repr(args[0]) + )) + class FormatterABC(with_metaclass(abc.ABCMeta, object)): @@ -262,6 +270,7 @@ class BaseFormatter(Configurable): """ format_type = Unicode('text/plain') + _return_type = string_types enabled = Bool(True, config=True) @@ -278,7 +287,7 @@ class BaseFormatter(Configurable): # The deferred-import type-specific printers. # Map (modulename, classname) pairs to the format functions. deferred_printers = Dict(config=True) - + @warn_format_error def __call__(self, obj): """Compute the format for an object.""" @@ -679,6 +688,8 @@ class PNGFormatter(BaseFormatter): format_type = Unicode('image/png') print_method = ObjectName('_repr_png_') + + _return_type = (bytes, unicode_type) class JPEGFormatter(BaseFormatter): @@ -696,6 +707,8 @@ class JPEGFormatter(BaseFormatter): print_method = ObjectName('_repr_jpeg_') + _return_type = (bytes, unicode_type) + class LatexFormatter(BaseFormatter): """A LaTeX formatter. diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 45ee860a9ee..7b194795cdb 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -276,7 +276,7 @@ var IPython = (function (IPython) { "json" : "application/json", "javascript" : "application/javascript", }; - + OutputArea.prototype.rename_keys = function (data, key_map) { var remapped = {}; for (var key in data) { @@ -285,7 +285,30 @@ var IPython = (function (IPython) { } return remapped; }; - + + + OutputArea.output_types = [ + 'application/javascript', + 'text/html', + 'text/latex', + 'image/svg+xml', + 'image/png', + 'image/jpeg', + 'text/plain' + ]; + + OutputArea.prototype.validate_output = function (json) { + // scrub invalid outputs + // TODO: right now everything is a string, but JSON really shouldn't be. + // nbformat 4 will fix that. + $.map(OutputArea.output_types, function(key){ + if (json[key] !== undefined && typeof json[key] !== 'string') { + console.log("Invalid type for " + key, json[key]); + delete json[key]; + } + }); + return json; + }; OutputArea.prototype.append_output = function (json) { this.expand(); @@ -295,6 +318,9 @@ var IPython = (function (IPython) { this.clear_output(false); needs_height_reset = true; } + + // validate output data types + json = this.validate_output(json); if (json.output_type === 'pyout') { this.append_pyout(json); diff --git a/IPython/html/tests/casperjs/test_cases/nb_roundtrip.js b/IPython/html/tests/casperjs/test_cases/nb_roundtrip.js index 9f5efbee235..4545d0c22d4 100644 --- a/IPython/html/tests/casperjs/test_cases/nb_roundtrip.js +++ b/IPython/html/tests/casperjs/test_cases/nb_roundtrip.js @@ -15,8 +15,8 @@ mime = { "javascript" : "application/javascript", }; -var black_dot_jpeg="\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\""; -var black_dot_png = '\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"'; +var black_dot_jpeg="u\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\""; +var black_dot_png = 'u\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"'; var svg = "\"\""; // helper function to ensure that the short_name is found in the toJSON diff --git a/IPython/html/tests/casperjs/test_cases/safe_append_output.js b/IPython/html/tests/casperjs/test_cases/safe_append_output.js new file mode 100644 index 00000000000..1217740a704 --- /dev/null +++ b/IPython/html/tests/casperjs/test_cases/safe_append_output.js @@ -0,0 +1,32 @@ +// +// Test validation in append_output +// +// Invalid output data is stripped and logged. +// + +casper.notebook_test(function () { + // this.printLog(); + var messages = []; + this.on('remote.message', function (msg) { + messages.push(msg); + }); + + this.evaluate(function () { + var cell = IPython.notebook.get_cell(0); + cell.set_text( "dp = get_ipython().display_pub\n" + + "dp.publish('test', {'text/plain' : '5', 'image/png' : 5})" + ); + cell.execute(); + }); + + this.wait_for_output(0); + this.on('remote.message', function () {}); + + this.then(function () { + var output = this.get_output_cell(0); + this.test.assert(messages.length > 0, "Captured log message"); + this.test.assertEquals(messages[messages.length-1], "Invalid type for image/png 5", "Logged Invalid type message"); + this.test.assertEquals(output['image/png'], undefined, "Non-string png data was stripped"); + this.test.assertEquals(output['text/plain'], '5', "text data is fine"); + }); +});