Skip to content

Commit

Permalink
PR: Use fastjsonschema if installed and add tests (#191)
Browse files Browse the repository at this point in the history
* Use fastjsonschema if installed and tests.

* Refactor code into a common validator

* Split classes and add reading the current validator from environment variable

* Add review comments.

* Update docs, readme and changelog and CI for tests

* Fix extras

* Add fastjsonshcema to test deps and update CI
  • Loading branch information
goanpeca committed Oct 6, 2020
1 parent 24e6636 commit 23272c1
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 119 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install test dependencies
run: |
pip install --upgrade pip setuptools
pip install nbformat[test]
pip install .[test]
pip install codecov
- name: Install nbformat
run: |
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

[![codecov.io](https://codecov.io/github/jupyter/nbformat/coverage.svg?branch=master)](https://codecov.io/github/jupyter/nbformat?branch=master)
[![Code Health](https://landscape.io/github/jupyter/nbformat/master/landscape.svg?style=flat)](https://landscape.io/github/jupyter/nbformat/master)


![CI Tests](https://github.com/jupyter/nbformat/workflows/Run%20tests/badge.svg)

`nbformat` contains the reference implementation of the [Jupyter Notebook format][],
and Python APIs for working with notebooks.
Expand All @@ -20,6 +19,16 @@ From the command line:
pip install nbformat
```

## Using a different json schema validator

You can install and use [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/) by running:

``` {.sourceCode .bash}
pip install nbformat[fast]
```

To enable fast validation with `fastjsonschema`, set the environment variable `NBFORMAT_VALIDATOR` to the value `fastjsonschema`.

## Python Version Support

This library supported python 2.7 and python 3.5+ for `4.x.x` releases. With python 2's end-of-life nbformat `5.x.x` is now python 3.5+ only. Support for 3.5 will be dropped when it's officially sunset by the python organization.
Expand Down
12 changes: 12 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,15 @@ bindings are available, and an in-memory store otherwise.
.. autoclass:: SQLiteSignatureStore

.. autoclass:: MemorySignatureStore

Optional fast validation
------------------------

You can install and use `fastjsonschema <https://horejsek.github.io/python-fastjsonschema/>`_ by running::

pip install nbformat[fast]


To enable fast validation with `fastjsonschema`, set the environment variable::

NBFORMAT_VALIDATOR="fastjsonschema"
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Changes in nbformat
In Development
==============

- Add optional support for using `fastjsonschema` as the JSON validation library.
To enable fast validation, install `fastjsonschema` and set the environment
variable `NBFORMAT_VALIDATOR` to the value `fastjsonschema`.

5.0.7
=====

Expand Down
82 changes: 82 additions & 0 deletions nbformat/json_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
"""
Common validator wrapper to provide a uniform usage of other schema validation
libraries.
"""

import os

import jsonschema
from jsonschema import Draft4Validator as _JsonSchemaValidator
from jsonschema import ValidationError

try:
import fastjsonschema
from fastjsonschema import JsonSchemaException as _JsonSchemaException
except ImportError:
fastjsonschema = None
_JsonSchemaException = ValidationError


class JsonSchemaValidator:
name = "jsonschema"

def __init__(self, schema):
self._schema = schema
self._default_validator = _JsonSchemaValidator(schema) # Default
self._validator = self._default_validator

def validate(self, data):
self._default_validator.validate(data)

def iter_errors(self, data, schema=None):
return self._default_validator.iter_errors(data, schema)


class FastJsonSchemaValidator(JsonSchemaValidator):
name = "fastjsonschema"

def __init__(self, schema):
self._validator = fastjsonschema.compile(schema)

def validate(self, data):
try:
self._validator(data)
except _JsonSchemaException as error:
raise ValidationError(error.message, schema_path=error.path)

def iter_errors(self, data, schema=None):
errors = []
validate_func = self._validator if schema is None else fastjsonschema.compile(schema)
try:
validate_func(data)
except _JsonSchemaException as error:
errors = [ValidationError(error.message, schema_path=error.path)]

return errors


_VALIDATOR_MAP = [
("fastjsonschema", fastjsonschema, FastJsonSchemaValidator),
("jsonschema", jsonschema, JsonSchemaValidator),
]
VALIDATORS = [item[0] for item in _VALIDATOR_MAP]


def _validator_for_name(validator_name):
if validator_name not in VALIDATORS:
raise ValueError("Invalid validator '{0}' value!\nValid values are: {1}".format(
validator_name, VALIDATORS))

for (name, module, validator_cls) in _VALIDATOR_MAP:
if module and validator_name == name:
return validator_cls


def get_current_validator():
"""
Return the default validator based on the value of an environment variable.
"""
validator_name = os.environ.get("NBFORMAT_VALIDATOR", "jsonschema")
return _validator_for_name(validator_name)
9 changes: 5 additions & 4 deletions nbformat/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
class TestsBase(unittest.TestCase):
"""Base tests class."""

def fopen(self, f, mode=u'r',encoding='utf-8'):
return io.open(os.path.join(self._get_files_path(), f), mode, encoding=encoding)
@classmethod
def fopen(cls, f, mode=u'r',encoding='utf-8'):
return io.open(os.path.join(cls._get_files_path(), f), mode, encoding=encoding)


def _get_files_path(self):
@classmethod
def _get_files_path(cls):
return os.path.dirname(__file__)
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
}
Loading

0 comments on commit 23272c1

Please sign in to comment.