diff --git a/.gitignore b/.gitignore index 6f294ab9..cc963fd9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ __pycache__ .coverage .cache .python-version +.idea diff --git a/nbformat/__init__.py b/nbformat/__init__.py index fa05fc27..595b9b02 100644 --- a/nbformat/__init__.py +++ b/nbformat/__init__.py @@ -48,7 +48,7 @@ class NBFormatError(ValueError): """) -def reads(s, as_version, **kwargs): +def reads(s, as_version, capture_validation_error=None, **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. @@ -64,6 +64,10 @@ def reads(s, as_version, **kwargs): The version of the notebook format to return. The notebook will be converted, if necessary. Pass nbformat.NO_CONVERT to prevent conversion. + capture_validation_error : dict, optional + If provided, a key of "ValidationError" with a + value of the ValidationError instance will be added + to the dictionary. Returns ------- @@ -77,10 +81,12 @@ def reads(s, as_version, **kwargs): validate(nb) except ValidationError as e: get_logger().error("Notebook JSON is invalid: %s", e) + if isinstance(capture_validation_error, dict): + capture_validation_error['ValidationError'] = e return nb -def writes(nb, version=NO_CONVERT, **kwargs): +def writes(nb, version=NO_CONVERT, capture_validation_error=None, **kwargs): """Write a notebook to a string in a given format in the given nbformat version. Any notebook format errors will be logged. @@ -93,6 +99,10 @@ def writes(nb, version=NO_CONVERT, **kwargs): The nbformat version to write. If unspecified, or specified as nbformat.NO_CONVERT, the notebook's own version will be used and no conversion performed. + capture_validation_error : dict, optional + If provided, a key of "ValidationError" with a + value of the ValidationError instance will be added + to the dictionary. Returns ------- @@ -107,10 +117,12 @@ def writes(nb, version=NO_CONVERT, **kwargs): validate(nb) except ValidationError as e: get_logger().error("Notebook JSON is invalid: %s", e) + if isinstance(capture_validation_error, dict): + capture_validation_error['ValidationError'] = e return versions[version].writes_json(nb, **kwargs) -def read(fp, as_version, **kwargs): +def read(fp, as_version, capture_validation_error=None, **kwargs): """Read a notebook from a file as a NotebookNode of the given version. The string can contain a notebook of any version. @@ -127,6 +139,10 @@ def read(fp, as_version, **kwargs): The version of the notebook format to return. The notebook will be converted, if necessary. Pass nbformat.NO_CONVERT to prevent conversion. + capture_validation_error : dict, optional + If provided, a key of "ValidationError" with a + value of the ValidationError instance will be added + to the dictionary. Returns ------- @@ -138,12 +154,12 @@ def read(fp, as_version, **kwargs): buf = fp.read() except AttributeError: with io.open(fp, encoding='utf-8') as f: - return reads(f.read(), as_version, **kwargs) + return reads(f.read(), as_version, capture_validation_error, **kwargs) - return reads(buf, as_version, **kwargs) + return reads(buf, as_version, capture_validation_error, **kwargs) -def write(nb, fp, version=NO_CONVERT, **kwargs): +def write(nb, fp, version=NO_CONVERT, capture_validation_error=None, **kwargs): """Write a notebook to a file in a given nbformat version. The file-like object must accept unicode input. @@ -160,8 +176,12 @@ def write(nb, fp, version=NO_CONVERT, **kwargs): If nb is not this version, it will be converted. If unspecified, or specified as nbformat.NO_CONVERT, the notebook's own version will be used and no conversion performed. + capture_validation_error : dict, optional + If provided, a key of "ValidationError" with a + value of the ValidationError instance will be added + to the dictionary. """ - s = writes(nb, version, **kwargs) + s = writes(nb, version, capture_validation_error, **kwargs) if isinstance(s, bytes): s = s.decode('utf8') diff --git a/nbformat/tests/test_api.py b/nbformat/tests/test_api.py index 7d9c0c70..dce53467 100644 --- a/nbformat/tests/test_api.py +++ b/nbformat/tests/test_api.py @@ -10,9 +10,11 @@ import unittest from .base import TestsBase +from jsonschema import ValidationError from tempfile import TemporaryDirectory from ..reader import get_version +from ..validator import isvalid from nbformat import read, current_nbformat, writes, write @@ -64,3 +66,34 @@ def test_read_write_pathlib_object(self): dest = pathlib.Path(td) / 'echidna.ipynb' write(nb, dest) assert os.path.isfile(dest) + + def test_capture_validation_error(self): + """Test that validation error can be captured on read() and write()""" + validation_error = {} + path = os.path.join(self._get_files_path(), u'invalid.ipynb') + nb = read(path, as_version=4, capture_validation_error=validation_error) + assert not isvalid(nb) + assert 'ValidationError' in validation_error + assert isinstance(validation_error['ValidationError'], ValidationError) + + validation_error = {} + with TemporaryDirectory() as td: + dest = os.path.join(td, 'invalid.ipynb') + write(nb, dest, capture_validation_error=validation_error) + assert os.path.isfile(dest) + assert 'ValidationError' in validation_error + assert isinstance(validation_error['ValidationError'], ValidationError) + + # Repeat with a valid notebook file + validation_error = {} + path = os.path.join(self._get_files_path(), u'test4.ipynb') + nb = read(path, as_version=4, capture_validation_error=validation_error) + assert isvalid(nb) + assert 'ValidationError' not in validation_error + + validation_error = {} + with TemporaryDirectory() as td: + dest = os.path.join(td, 'test4.ipynb') + write(nb, dest, capture_validation_error=validation_error) + assert os.path.isfile(dest) + assert 'ValidationError' not in validation_error