diff --git a/IPython/nbformat/current.py b/IPython/nbformat/current.py index 5223c4e..5688e50 100644 --- a/IPython/nbformat/current.py +++ b/IPython/nbformat/current.py @@ -28,13 +28,17 @@ from IPython.nbformat.v3 import ( NotebookNode, new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet, parse_filename, new_metadata, new_author, new_heading_cell, nbformat, - nbformat_minor, to_notebook_json + nbformat_minor, nbformat_schema, to_notebook_json ) from IPython.nbformat import v3 as _v_latest from .reader import reads as reader_reads from .reader import versions from .convert import convert +from .validator import validate + +import logging +logger = logging.getLogger('NotebookApp') #----------------------------------------------------------------------------- # Code @@ -44,6 +48,7 @@ current_nbformat = nbformat current_nbformat_minor = nbformat_minor current_nbformat_module = _v_latest.__name__ + def docstring_nbformat_mod(func): """Decorator for docstrings referring to classes/functions accessed through nbformat.current. @@ -74,13 +79,33 @@ def parse_py(s, **kwargs): return nbf, nbm, s -def reads_json(s, **kwargs): - """Read a JSON notebook from a string and return the NotebookNode object.""" - return convert(reader_reads(s), current_nbformat) +def reads_json(nbjson, **kwargs): + """Read a JSON notebook from a string and return the NotebookNode + object. Report if any JSON format errors are detected. + + """ + nb = reader_reads(nbjson, **kwargs) + nb_current = convert(nb, current_nbformat) + errors = validate(nb_current) + if errors: + logger.error( + "Notebook JSON is invalid (%d errors detected during read)", + len(errors)) + return nb_current def writes_json(nb, **kwargs): - return versions[current_nbformat].writes_json(nb, **kwargs) + """Take a NotebookNode object and write out a JSON string. Report if + any JSON format errors are detected. + + """ + errors = validate(nb) + if errors: + logger.error( + "Notebook JSON is invalid (%d errors detected during write)", + len(errors)) + nbjson = versions[current_nbformat].writes_json(nb, **kwargs) + return nbjson def reads_py(s, **kwargs): diff --git a/IPython/nbformat/tests/invalid.ipynb b/IPython/nbformat/tests/invalid.ipynb new file mode 100644 index 0000000..d53686d --- /dev/null +++ b/IPython/nbformat/tests/invalid.ipynb @@ -0,0 +1,152 @@ +{ + "metadata": { + "cell_tags": [ + [ + "", + null + ] + ], + "name": 0 + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "heading", + "level": 1, + "source": [ + "nbconvert latex test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis." + ] + }, + { + "cell_type": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Printed Using Python" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "print(\"hello\")" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "hello\n" + ] + } + ], + "prompt_number": 1 + }, + { + "cell_type": "heading", + "level": 1000, + "metadata": {}, + "source": [ + "Pyout" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from IPython.display import HTML\n", + "HTML(\"\"\"\n", + "\n", + "HTML\n", + "\"\"\")" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "html": [ + "\n", + "\n", + "HTML\n" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 3, + "text": [ + "" + ] + } + ], + "prompt_number": 3 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "%%javascript\n", + "console.log(\"hi\");" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "javascript": [ + "console.log(\"hi\");" + ], + "metadata": {}, + "output_type": "display_data", + "text": [ + "" + ] + } + ], + "prompt_number": 7 + }, + { + "cell_type": "heading", + "level": 3, + "metadata": {} + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from IPython.display import Image\n", + "Image(\"http://ipython.org/_static/IPy_header.png\")" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "png": "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\n", + "prompt_number": 6, + "text": [ + "" + ] + } + ], + "prompt_number": 6 + } + ], + "metadata": {} + } + ] +} diff --git a/IPython/nbformat/tests/test_validator.py b/IPython/nbformat/tests/test_validator.py new file mode 100644 index 0000000..a4c5975 --- /dev/null +++ b/IPython/nbformat/tests/test_validator.py @@ -0,0 +1,73 @@ +""" +Contains tests class for validator.py +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2014 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os + +from .base import TestsBase +from IPython.external.jsonschema import SchemaError +from ..current import read +from ..validator import schema_path, isvalid, validate, resolve_ref + + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- + +class TestValidator(TestsBase): + + def test_schema_path(self): + """Test that the schema path exists""" + self.assertEqual(os.path.exists(schema_path), True) + + def test_nb2(self): + """Test that a v2 notebook converted to v3 passes validation""" + with self.fopen(u'test2.ipynb', u'r') as f: + nb = read(f, u'json') + self.assertEqual(validate(nb), []) + self.assertEqual(isvalid(nb), True) + + def test_nb3(self): + """Test that a v3 notebook passes validation""" + with self.fopen(u'test3.ipynb', u'r') as f: + nb = read(f, u'json') + self.assertEqual(validate(nb), []) + self.assertEqual(isvalid(nb), True) + + def test_invalid(self): + """Test than an invalid notebook does not pass validation""" + # this notebook has a few different errors: + # - the name is an integer, rather than a string + # - one cell is missing its source + # - one cell has an invalid level + with self.fopen(u'invalid.ipynb', u'r') as f: + nb = read(f, u'json') + self.assertEqual(len(validate(nb)), 3) + self.assertEqual(isvalid(nb), False) + + def test_resolve_ref(self): + """Test that references are correctly resolved""" + # make sure it resolves the ref correctly + json = {"abc": "def", "ghi": {"$ref": "/abc"}} + resolved = resolve_ref(json) + self.assertEqual(resolved, {"abc": "def", "ghi": "def"}) + + # make sure it throws an error if the ref is not by itself + json = {"abc": "def", "ghi": {"$ref": "/abc", "foo": "bar"}} + with self.assertRaises(SchemaError): + resolved = resolve_ref(json) + + # make sure it can handle json with no reference + json = {"abc": "def"} + resolved = resolve_ref(json) + self.assertEqual(resolved, json) diff --git a/IPython/nbformat/v3/__init__.py b/IPython/nbformat/v3/__init__.py index 2bcc4bc..b874956 100644 --- a/IPython/nbformat/v3/__init__.py +++ b/IPython/nbformat/v3/__init__.py @@ -19,7 +19,8 @@ Authors: from .nbbase import ( NotebookNode, new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet, - new_metadata, new_author, new_heading_cell, nbformat, nbformat_minor + new_metadata, new_author, new_heading_cell, nbformat, nbformat_minor, + nbformat_schema ) from .nbjson import reads as reads_json, writes as writes_json diff --git a/IPython/nbformat/v3/nbbase.py b/IPython/nbformat/v3/nbbase.py index fcd86f6..f27572d 100644 --- a/IPython/nbformat/v3/nbbase.py +++ b/IPython/nbformat/v3/nbbase.py @@ -22,6 +22,7 @@ from IPython.utils.py3compat import cast_unicode, unicode_type # Change this when incrementing the nbformat version nbformat = 3 nbformat_minor = 0 +nbformat_schema = 'v3.withref.json' class NotebookNode(Struct): pass diff --git a/IPython/nbformat/v3/v3.withref.json b/IPython/nbformat/v3/v3.withref.json index b16036f..9a711a2 100644 --- a/IPython/nbformat/v3/v3.withref.json +++ b/IPython/nbformat/v3/v3.withref.json @@ -38,6 +38,13 @@ "id": "nbformat", "required": true }, + "orig_nbformat": { + "description": "Original notebook format, major number.", + "type": "integer", + "minimum": 1, + "id": "orig_nbformat", + "required": false + }, "worksheets": { "description": "Array of worksheet, not used by the current implementation of ipython yet", "type": "array", @@ -64,7 +71,7 @@ "type": "object", "description": "metadata of the current worksheet", "id": "metadata", - "required": true + "required": false } } }, @@ -92,11 +99,11 @@ "metadata": { "type": "object", "id": "metadata", - "required": true + "required": false }, "source": { "description": "for code cell, the source code", - "type": "array", + "type": ["array", "string"], "id": "source", "required": true, "items": @@ -133,7 +140,7 @@ "metadata": { "type": "object", "id": "metadata", - "required": true + "required": false }, "collapsed": { "type": "boolean", @@ -141,7 +148,7 @@ }, "input": { "description": "user input for text cells", - "type": "array", + "type": ["array", "string"], "id": "input", "required": true, "items": @@ -158,7 +165,7 @@ }, "prompt_number": { "type": ["integer","null"], - "required": true, + "required": false, "minimum": 0 }, "language": { diff --git a/IPython/nbformat/v3/validator.py b/IPython/nbformat/v3/validator.py deleted file mode 100755 index bc037cc..0000000 --- a/IPython/nbformat/v3/validator.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import print_function -#!/usr/bin/env python -# -*- coding: utf8 -*- -import argparse -import traceback -import json - -from IPython.external.jsonschema import Draft3Validator, validate, ValidationError -import IPython.external.jsonpointer as jsonpointer -from IPython.utils.py3compat import iteritems - -def nbvalidate(nbjson, schema='v3.withref.json', key=None,verbose=True): - v3schema = resolve_ref(json.load(open(schema,'r'))) - if key : - v3schema = jsonpointer.resolve_pointer(v3schema,key) - errors = 0 - v = Draft3Validator(v3schema); - for error in v.iter_errors(nbjson): - errors = errors + 1 - if verbose: - print(error) - return errors - -def resolve_ref(json, base=None): - """return a json with resolved internal references - - only support local reference to the same json - """ - if not base : - base = json - - temp = None - if type(json) is list: - temp = []; - for item in json: - temp.append(resolve_ref(item, base=base)) - elif type(json) is dict: - temp = {}; - for key,value in iteritems(json): - if key == '$ref': - return resolve_ref(jsonpointer.resolve_pointer(base,value), base=base) - else : - temp[key]=resolve_ref(value, base=base) - else : - return json - return temp - -def convert(namein, nameout, indent=2): - """resolve the references of namein, save the result in nameout""" - jsn = None - with open(namein) as file : - jsn = json.load(file) - v = resolve_ref(jsn, base=jsn) - x = jsonpointer.resolve_pointer(v, '/notebook') - with open(nameout,'w') as file: - json.dump(x,file,indent=indent) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-s', '--schema', - type=str, default='v3.withref.json') - - parser.add_argument('-k', '--key', - type=str, default='/notebook', - help='subkey to extract json schema from json file') - - parser.add_argument("-v", "--verbose", action="store_true", - help="increase output verbosity") - - parser.add_argument('filename', - type=str, - help="file to validate", - nargs='*', - metavar='names') - - args = parser.parse_args() - for name in args.filename : - nerror = nbvalidate(json.load(open(name,'r')), - schema=args.schema, - key=args.key, - verbose=args.verbose) - if nerror is 0: - print(u"[Pass]",name) - else : - print(u"[ ]",name,'(%d)'%(nerror)) - if args.verbose : - print('==================================================') - - diff --git a/IPython/nbformat/validator.py b/IPython/nbformat/validator.py new file mode 100644 index 0000000..e7dae33 --- /dev/null +++ b/IPython/nbformat/validator.py @@ -0,0 +1,91 @@ +from __future__ import print_function +import json +import os + +from IPython.external.jsonschema import Draft3Validator, SchemaError +import IPython.external.jsonpointer as jsonpointer +from IPython.utils.py3compat import iteritems + + +from .current import nbformat, nbformat_schema +schema_path = os.path.join( + os.path.dirname(__file__), "v%d" % nbformat, nbformat_schema) + + +def isvalid(nbjson): + """Checks whether the given notebook JSON conforms to the current + notebook format schema. Returns True if the JSON is valid, and + False otherwise. + + To see the individual errors that were encountered, please use the + `validate` function instead. + + """ + + errors = validate(nbjson) + return errors == [] + + +def validate(nbjson): + """Checks whether the given notebook JSON conforms to the current + notebook format schema, and returns the list of errors. + + """ + + # load the schema file + with open(schema_path, 'r') as fh: + schema_json = json.load(fh) + + # resolve internal references + v3schema = resolve_ref(schema_json) + v3schema = jsonpointer.resolve_pointer(v3schema, '/notebook') + + # count how many errors there are + v = Draft3Validator(v3schema) + errors = list(v.iter_errors(nbjson)) + return errors + + +def resolve_ref(json, schema=None): + """Resolve internal references within the given JSON. This essentially + means that dictionaries of this form: + + {"$ref": "/somepointer"} + + will be replaced with the resolved reference to `/somepointer`. + This only supports local reference to the same JSON file. + + """ + + if not schema: + schema = json + + # if it's a list, resolve references for each item in the list + if type(json) is list: + resolved = [] + for item in json: + resolved.append(resolve_ref(item, schema=schema)) + + # if it's a dictionary, resolve references for each item in the + # dictionary + elif type(json) is dict: + resolved = {} + for key, ref in iteritems(json): + + # if the key is equal to $ref, then replace the entire + # dictionary with the resolved value + if key == '$ref': + if len(json) != 1: + raise SchemaError( + "objects containing a $ref should only have one item") + pointer = jsonpointer.resolve_pointer(schema, ref) + resolved = resolve_ref(pointer, schema=schema) + + else: + resolved[key] = resolve_ref(ref, schema=schema) + + # otherwise it's a normal object, so just return it + else: + resolved = json + + return resolved diff --git a/MANIFEST.in b/MANIFEST.in index 57a79d0..1e9a93b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,7 @@ prune IPython/html/static/mathjax # Include some specific files and data resources we need include IPython/.git_commit_info.ini include IPython/qt/console/resources/icon/IPythonConsole.svg +include IPython/nbformat/v3/v3.withref.json # Documentation graft docs diff --git a/setupbase.py b/setupbase.py index 7423aff..ee9cb32 100644 --- a/setupbase.py +++ b/setupbase.py @@ -187,7 +187,7 @@ def find_package_data(): 'IPython.nbconvert' : nbconvert_templates + ['tests/files/*.*', 'exporters/tests/files/*.*'], 'IPython.nbconvert.filters' : ['marked.js'], - 'IPython.nbformat' : ['tests/*.ipynb'] + 'IPython.nbformat' : ['tests/*.ipynb','v3/v3.withref.json'] } return package_data