Skip to content

Commit

Permalink
Use fastjsonschema if installed and tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
goanpeca committed Sep 24, 2020
1 parent 9b9f46f commit ea9ae9d
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ sudo: false
install:
- pip install --upgrade setuptools pip
- pip install --upgrade pytest
- pip install --upgrade fastjsonschema
- pip install . codecov
- pip install nbformat[test]
- pip freeze
script:
- py.test -v --cov nbformat nbformat
- pip uninstall fastjsonschema --yes
- py.test -v --cov nbformat nbformat
after_success:
- codecov
matrix:
Expand Down
3 changes: 3 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ install:
- 'SET "PATH=%PYTHON%\\Scripts;%PATH%"'
# Install our package:
- pip install codecov
- pip install fastjsonschema
- 'pip install --upgrade ".[test]"'

build: off

# to run your custom scripts instead of automatic tests
test_script:
- 'py.test -v --cov nbformat nbformat'
- 'pip uninstall fastjsonschema --yes'
- 'py.test -v --cov nbformat nbformat'

on_success:
- codecov
4 changes: 2 additions & 2 deletions nbformat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def reads(s, as_version, **kwargs):
if as_version is not NO_CONVERT:
nb = convert(nb, as_version)
try:
validate(nb)
validate(nb, use_fast=True)
except ValidationError as e:
get_logger().error("Notebook JSON is invalid: %s", e)
return nb
Expand Down Expand Up @@ -104,7 +104,7 @@ def writes(nb, version=NO_CONVERT, **kwargs):
else:
version, _ = reader.get_version(nb)
try:
validate(nb)
validate(nb, use_fast=True)
except ValidationError as e:
get_logger().error("Notebook JSON is invalid: %s", e)
return versions[version].writes_json(nb, **kwargs)
Expand Down
46 changes: 46 additions & 0 deletions nbformat/tests/many_tracebacks.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'iAmNotDefined' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-22-56e1109ae320>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0miAmNotDefined\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mNameError\u001b[0m: name 'iAmNotDefined' is not defined"
]
}
],
"source": [
"# Imagine this cell called a function which runs things on a cluster and you have an error"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.5"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
30 changes: 30 additions & 0 deletions nbformat/tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
# Distributed under the terms of the Modified BSD License.

import os
import time

from .base import TestsBase
from jsonschema import ValidationError
from nbformat import read
from ..validator import isvalid, validate, iter_validate

try:
import fastjsonschema
except ImportError:
fastjsonschema = None


class TestValidator(TestsBase):

Expand Down Expand Up @@ -118,3 +124,27 @@ def test_validation_no_version(self):
"""Test that an invalid notebook with no version fails validation"""
with self.assertRaises(ValidationError) as e:
validate({'invalid': 'notebook'})

def test_fast_validation(self):
"""
Test that valid notebook with too many outputs is parsed ~12 times
faster with fastjsonschema.
"""
if fastjsonschema:
with self.fopen(u'many_tracebacks.ipynb', u'r') as f:
nb = read(f, as_version=4)

# Multiply output
base_output = nb["cells"][0]["outputs"][0]
for i in range(50000):
nb["cells"][0]["outputs"].append(base_output)

start_time = time.time()
validate(nb, use_fast=True)
fast_time = time.time() - start_time

start_time = time.time()
validate(nb)
slow_time = time.time() - start_time

self.assertGreater(slow_time / fast_time, 12)
52 changes: 43 additions & 9 deletions nbformat/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@
"""
raise ImportError(verbose_msg) from e

# Use fastjsonschema if installed
try:
import fastjsonschema
from fastjsonschema import JsonSchemaException
except ImportError:
fastjsonschema = None
JsonSchemaException = ValidationError

from ipython_genutils.importstring import import_item
from .reader import get_version, reads


validators = {}
fast_validators = {}

def _relax_additional_properties(obj):
"""relax any `additionalProperties`"""
Expand All @@ -50,7 +59,7 @@ def _allow_undefined(schema):
)
return schema

def get_validator(version=None, version_minor=None, relax_add_props=False):
def get_validator(version=None, version_minor=None, relax_add_props=False, use_fast=False):
"""Load the JSON schema into a Validator"""
if version is None:
from . import current_nbformat
Expand All @@ -66,7 +75,7 @@ def get_validator(version=None, version_minor=None, relax_add_props=False):
if version_tuple not in validators:
try:
schema_json = _get_schema_json(v, version=version, version_minor=version_minor)
except AttributeError:
except AttributeError as e:
return None

if current_minor < version_minor:
Expand All @@ -77,6 +86,10 @@ def get_validator(version=None, version_minor=None, relax_add_props=False):

validators[version_tuple] = Validator(schema_json)

# If fastjsonschema is installed use it to validate
if use_fast and fastjsonschema is not None and version_tuple not in fast_validators:
fast_validators[version_tuple] = fastjsonschema.compile(schema_json)

if relax_add_props:
try:
schema_json = _get_schema_json(v, version=version, version_minor=version_minor)
Expand All @@ -88,7 +101,15 @@ def get_validator(version=None, version_minor=None, relax_add_props=False):
schema_json = _relax_additional_properties(schema_json)

validators[version_tuple] = Validator(schema_json)
return validators[version_tuple]

# If fastjsonschema is installed use it to validate
if use_fast and fastjsonschema is not None:
fast_validators[version_tuple] = fastjsonschema.compile(schema_json)

if use_fast and fastjsonschema is not None:
return fast_validators[version_tuple]
else:
return validators[version_tuple]


def _get_schema_json(v, version=None, version_minor=None):
Expand Down Expand Up @@ -241,7 +262,7 @@ def better_validation_error(error, version, version_minor):


def validate(nbdict=None, ref=None, version=None, version_minor=None,
relax_add_props=False, nbjson=None):
relax_add_props=False, nbjson=None, use_fast=False):
"""Checks whether the given notebook dict-like object
conforms to the relevant notebook format schema.
Expand Down Expand Up @@ -270,10 +291,22 @@ def validate(nbdict=None, ref=None, version=None, version_minor=None,
if version is None:
version, version_minor = 1, 0

for error in iter_validate(nbdict, ref=ref, version=version,
version_minor=version_minor,
relax_add_props=relax_add_props):
raise error
validator = get_validator(version, version_minor, relax_add_props=relax_add_props,
use_fast=True)

if fastjsonschema is not None and use_fast:
if validator is None:
raise ValidationError("No schema for validating v%s notebooks" % version)

try:
validator(nbdict)
except JsonSchemaException as e:
raise ValidationError(e.message, schema_path=e.path)
else:
for error in iter_validate(nbdict, ref=ref, version=version,
version_minor=version_minor,
relax_add_props=relax_add_props):
raise error


def iter_validate(nbdict=None, ref=None, version=None, version_minor=None,
Expand All @@ -294,7 +327,8 @@ def iter_validate(nbdict=None, ref=None, version=None, version_minor=None,
if version is None:
version, version_minor = get_version(nbdict)

validator = get_validator(version, version_minor, relax_add_props=relax_add_props)
validator = get_validator(version, version_minor, relax_add_props=relax_add_props,
use_fast=False)

if validator is None:
# no validator
Expand Down

0 comments on commit ea9ae9d

Please sign in to comment.