Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

various display type validations #4565

Merged
merged 10 commits into from Jan 13, 2014
23 changes: 17 additions & 6 deletions IPython/core/display.py
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -398,7 +409,7 @@ def _repr_svg_(self):
return self.data


class JSON(DisplayObject):
class JSON(TextDisplayObject):

def _repr_json_(self):
return self.data
Expand All @@ -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.
Expand Down
21 changes: 17 additions & 4 deletions IPython/core/formatters.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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)):
Expand Down Expand Up @@ -262,6 +270,7 @@ class BaseFormatter(Configurable):
"""

format_type = Unicode('text/plain')
_return_type = string_types

enabled = Bool(True, config=True)

Expand All @@ -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."""
Expand Down Expand Up @@ -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):
Expand All @@ -696,6 +707,8 @@ class JPEGFormatter(BaseFormatter):

print_method = ObjectName('_repr_jpeg_')

_return_type = (bytes, unicode_type)


class LatexFormatter(BaseFormatter):
"""A LaTeX formatter.
Expand Down
30 changes: 28 additions & 2 deletions IPython/html/static/notebook/js/outputarea.js
Expand Up @@ -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) {
Expand All @@ -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();
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions IPython/html/tests/casperjs/test_cases/nb_roundtrip.js
Expand Up @@ -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 = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";

// helper function to ensure that the short_name is found in the toJSON
Expand Down
32 changes: 32 additions & 0 deletions 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");
});
});