diff --git a/IPython/nbformat/__init__.py b/IPython/nbformat/__init__.py index e69de29..e6b1309 100644 --- a/IPython/nbformat/__init__.py +++ b/IPython/nbformat/__init__.py @@ -0,0 +1,143 @@ +"""The IPython notebook format + +Use this module to read or write notebook files as particular nbformat versions. +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from IPython.utils.log import get_logger + +from . import v1 +from . import v2 +from . import v3 +from . import v4 + +versions = { + 1: v1, + 2: v2, + 3: v3, + 4: v4, +} + +from .validator import validate, ValidationError +from .converter import convert +from . import reader + +from .v4 import ( + nbformat as current_nbformat, + nbformat_minor as current_nbformat_minor, +) + +class NBFormatError(ValueError): + pass + +# no-conversion singleton +NO_CONVERT = object() + +def reads(s, as_version, **kwargs): + """Read a notebook from a string and return the NotebookNode object as the given version. + + The string can contain a notebook of any version. + The notebook will be returned `as_version`, converting, if necessary. + + Notebook format errors will be logged. + + Parameters + ---------- + s : unicode + The raw unicode string to read the notebook from. + as_version : int + The version of the notebook format to return. + The notebook will be converted, if necessary. + Pass nbformat.NO_CONVERT to prevent conversion. + + Returns + ------- + nb : NotebookNode + The notebook that was read. + """ + nb = reader.reads(s, **kwargs) + if as_version is not NO_CONVERT: + nb = convert(nb, as_version) + try: + validate(nb) + except ValidationError as e: + get_logger().error("Notebook JSON is invalid: %s", e) + return nb + + +def writes(nb, version, **kwargs): + """Write a notebook to a string in a given format in the given nbformat version. + + Any notebook format errors will be logged. + + Parameters + ---------- + nb : NotebookNode + The notebook to write. + version : int + The nbformat version to write. + If nb is not this version, it will be converted. + Pass nbformat.NO_CONVERT to prevent conversion. + + Returns + ------- + s : unicode + The notebook as a JSON string. + """ + if version is not NO_CONVERT: + nb = convert(nb, version) + else: + version, _ = reader.get_version(nb) + try: + validate(nb) + except ValidationError as e: + get_logger().error("Notebook JSON is invalid: %s", e) + return versions[version].writes_json(nb, **kwargs) + + +def read(fp, as_version, **kwargs): + """Read a notebook from a file as a NotebookNode of the given version. + + The string can contain a notebook of any version. + The notebook will be returned `as_version`, converting, if necessary. + + Notebook format errors will be logged. + + Parameters + ---------- + fp : file + Any file-like object with a read method. + as_version: int + The version of the notebook format to return. + The notebook will be converted, if necessary. + Pass nbformat.NO_CONVERT to prevent conversion. + + Returns + ------- + nb : NotebookNode + The notebook that was read. + """ + return reads(fp.read(), as_version, **kwargs) + + +def write(fp, nb, version, **kwargs): + """Write a notebook to a file in a given nbformat version. + + The file-like object must accept unicode input. + + Parameters + ---------- + fp : file + Any file-like object with a write method that accepts unicode. + nb : NotebookNode + The notebook to write. + version : int + The nbformat version to write. + If nb is not this version, it will be converted. + """ + s = writes(nb, version, **kwargs) + if isinstance(s, bytes): + s = s.decode('utf8') + return fp.write(s) diff --git a/IPython/nbformat/convert.py b/IPython/nbformat/converter.py similarity index 94% rename from IPython/nbformat/convert.py rename to IPython/nbformat/converter.py index 5a2b86d..a2ea892 100644 --- a/IPython/nbformat/convert.py +++ b/IPython/nbformat/converter.py @@ -3,7 +3,8 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from .reader import get_version, versions +from . import versions +from .reader import get_version def convert(nb, to_version): @@ -24,7 +25,7 @@ def convert(nb, to_version): # Get input notebook version. (version, version_minor) = get_version(nb) - # Check if destination is current version, if so return contents + # Check if destination is target version, if so return contents if version == to_version: return nb diff --git a/IPython/nbformat/current.py b/IPython/nbformat/current.py index 0f796fd..2f5f832 100644 --- a/IPython/nbformat/current.py +++ b/IPython/nbformat/current.py @@ -17,8 +17,8 @@ from IPython.nbformat.v3 import ( from IPython.nbformat import v3 as _v_latest from .reader import reads as reader_reads -from .reader import versions -from .convert import convert +from . import versions +from .converter import convert from .validator import validate, ValidationError from IPython.utils.log import get_logger diff --git a/IPython/nbformat/reader.py b/IPython/nbformat/reader.py index 158e071..20ddaef 100644 --- a/IPython/nbformat/reader.py +++ b/IPython/nbformat/reader.py @@ -5,19 +5,6 @@ import json -from . import v1 -from . import v2 -from . import v3 -from . import v4 - -versions = { - 1: v1, - 2: v2, - 3: v3, - 4: v4, - } - - class NotJSONError(ValueError): pass @@ -66,7 +53,7 @@ def reads(s, **kwargs): nb : NotebookNode The notebook that was read. """ - from .current import NBFormatError + from . import versions, NBFormatError nb_dict = parse_json(s, **kwargs) (major, minor) = get_version(nb_dict) diff --git a/IPython/nbformat/sign.py b/IPython/nbformat/sign.py index 2005be1..17d9e1f 100644 --- a/IPython/nbformat/sign.py +++ b/IPython/nbformat/sign.py @@ -10,12 +10,13 @@ from hmac import HMAC import io import os +from IPython.utils.io import atomic_writing from IPython.utils.py3compat import string_types, unicode_type, cast_bytes from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool from IPython.config import LoggingConfigurable, MultipleInstanceError from IPython.core.application import BaseIPythonApplication, base_flags -from .current import read, write +from . import read, write, NO_CONVERT try: # Python 3 @@ -278,14 +279,14 @@ class TrustNotebookApp(BaseIPythonApplication): self.log.error("Notebook missing: %s" % notebook_path) self.exit(1) with io.open(notebook_path, encoding='utf8') as f: - nb = read(f, 'json') + nb = read(f, NO_CONVERT) if self.notary.check_signature(nb): print("Notebook already signed: %s" % notebook_path) else: print("Signing notebook: %s" % notebook_path) self.notary.sign(nb) - with io.open(notebook_path, 'w', encoding='utf8') as f: - write(nb, f, 'json') + with atomic_writing(notebook_path) as f: + write(f, nb, NO_CONVERT) def generate_new_key(self): """Generate a new notebook signature key""" diff --git a/IPython/nbformat/tests/test_current.py b/IPython/nbformat/tests/test_api.py similarity index 71% rename from IPython/nbformat/tests/test_current.py rename to IPython/nbformat/tests/test_api.py index 818447a..1fb1585 100644 --- a/IPython/nbformat/tests/test_current.py +++ b/IPython/nbformat/tests/test_api.py @@ -1,29 +1,25 @@ -""" -Contains tests class for current.py -""" +"""Test the APIs at the top-level of nbformat""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import io import json -import tempfile from .base import TestsBase from ..reader import get_version -from ..current import read, current_nbformat, validate, writes +from IPython.nbformat import read, current_nbformat, writes -class TestCurrent(TestsBase): +class TestAPI(TestsBase): def test_read(self): """Can older notebooks be opened and automatically converted to the current nbformat?""" # Open a version 2 notebook. - with self.fopen(u'test2.ipynb', u'r') as f: - nb = read(f) + with self.fopen(u'test2.ipynb', 'r') as f: + nb = read(f, as_version=current_nbformat) # Check that the notebook was upgraded to the latest version automatically. (major, minor) = get_version(nb) @@ -33,7 +29,7 @@ class TestCurrent(TestsBase): """dowgrade a v3 notebook to v2""" # Open a version 3 notebook. with self.fopen(u'test3.ipynb', 'r') as f: - nb = read(f, u'json') + nb = read(f, as_version=3) jsons = writes(nb, version=2) nb2 = json.loads(jsons) diff --git a/IPython/nbformat/tests/test_convert.py b/IPython/nbformat/tests/test_convert.py index 19c8b35..06ca145 100644 --- a/IPython/nbformat/tests/test_convert.py +++ b/IPython/nbformat/tests/test_convert.py @@ -5,9 +5,9 @@ from .base import TestsBase -from ..convert import convert +from ..converter import convert from ..reader import read, get_version -from ..current import current_nbformat +from .. import current_nbformat class TestConvert(TestsBase): diff --git a/IPython/nbformat/tests/test_sign.py b/IPython/nbformat/tests/test_sign.py index 5534105..286a10a 100644 --- a/IPython/nbformat/tests/test_sign.py +++ b/IPython/nbformat/tests/test_sign.py @@ -3,10 +3,9 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from .. import sign from .base import TestsBase -from ..current import read +from IPython.nbformat import read, sign from IPython.core.getipython import get_ipython @@ -18,7 +17,7 @@ class TestNotary(TestsBase): profile_dir=get_ipython().profile_dir ) with self.fopen(u'test3.ipynb', u'r') as f: - self.nb = read(f, u'json') + self.nb = read(f, as_version=4) def test_algorithms(self): last_sig = '' diff --git a/IPython/nbformat/tests/test_validator.py b/IPython/nbformat/tests/test_validator.py index 92b04fe..a084282 100644 --- a/IPython/nbformat/tests/test_validator.py +++ b/IPython/nbformat/tests/test_validator.py @@ -7,7 +7,7 @@ import os from .base import TestsBase from jsonschema import ValidationError -from ..current import read +from IPython.nbformat import read from ..validator import isvalid, validate @@ -16,21 +16,21 @@ class TestValidator(TestsBase): def test_nb2(self): """Test that a v2 notebook converted to current passes validation""" with self.fopen(u'test2.ipynb', u'r') as f: - nb = read(f, u'json') + nb = read(f, as_version=4) 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') + nb = read(f, as_version=4) validate(nb) self.assertEqual(isvalid(nb), True) def test_nb4(self): """Test that a v4 notebook passes validation""" with self.fopen(u'test4.ipynb', u'r') as f: - nb = read(f, u'json') + nb = read(f, as_version=4) validate(nb) self.assertEqual(isvalid(nb), True) @@ -41,7 +41,7 @@ class TestValidator(TestsBase): # - invalid cell type # - invalid output_type with self.fopen(u'invalid.ipynb', u'r') as f: - nb = read(f, u'json') + nb = read(f, as_version=4) with self.assertRaises(ValidationError): validate(nb) self.assertEqual(isvalid(nb), False) @@ -49,7 +49,7 @@ class TestValidator(TestsBase): def test_future(self): """Test than a notebook from the future with extra keys passes validation""" with self.fopen(u'test4plus.ipynb', u'r') as f: - nb = read(f) + nb = read(f, as_version=4) with self.assertRaises(ValidationError): validate(nb, version=4) diff --git a/IPython/nbformat/v4/convert.py b/IPython/nbformat/v4/convert.py index a73f2e6..37e5330 100644 --- a/IPython/nbformat/v4/convert.py +++ b/IPython/nbformat/v4/convert.py @@ -16,7 +16,7 @@ from IPython.utils.log import get_logger def _warn_if_invalid(nb, version): """Log validation errors, if there are any.""" - from IPython.nbformat.current import validate, ValidationError + from IPython.nbformat import validate, ValidationError try: validate(nb, version=version) except ValidationError as e: diff --git a/IPython/nbformat/v4/nbbase.py b/IPython/nbformat/v4/nbbase.py index 925992f..586f399 100644 --- a/IPython/nbformat/v4/nbbase.py +++ b/IPython/nbformat/v4/nbbase.py @@ -19,7 +19,7 @@ nbformat_schema = 'nbformat.v4.schema.json' def validate(node, ref=None): """validate a v4 node""" - from ..current import validate + from .. import validate return validate(node, ref=ref, version=nbformat) diff --git a/IPython/nbformat/v4/tests/test_convert.py b/IPython/nbformat/v4/tests/test_convert.py index 3cfe8da..4e9c0e5 100644 --- a/IPython/nbformat/v4/tests/test_convert.py +++ b/IPython/nbformat/v4/tests/test_convert.py @@ -2,7 +2,7 @@ import copy import nose.tools as nt -from IPython.nbformat.current import validate +from IPython.nbformat import validate from .. import convert from . import nbexamples diff --git a/IPython/nbformat/validator.py b/IPython/nbformat/validator.py index 040ce66..a4fcd86 100644 --- a/IPython/nbformat/validator.py +++ b/IPython/nbformat/validator.py @@ -43,7 +43,8 @@ def _relax_additional_properties(obj): def get_validator(version=None, version_minor=None): """Load the JSON schema into a Validator""" if version is None: - from .current import nbformat as version + from .. import current_nbformat + version = current_nbformat v = import_item("IPython.nbformat.v%s" % version) current_minor = v.nbformat_minor