diff --git a/altair/utils/display.py b/altair/utils/display.py index 92d7c7835..bcf8232b3 100644 --- a/altair/utils/display.py +++ b/altair/utils/display.py @@ -4,10 +4,9 @@ from typing import Callable, Dict import uuid -from jsonschema import validate - from .plugin_registry import PluginRegistry from .mimebundle import spec_to_mimebundle +from .schemapi import validate_jsonschema # ============================================================================== @@ -126,7 +125,7 @@ def _validate(self): # type: () -> None """Validate the spec against the schema.""" schema_dict = json.loads(pkgutil.get_data(*self.schema_path).decode("utf-8")) - validate(self.spec, schema_dict) + validate_jsonschema(self.spec, schema_dict) def _repr_mimebundle_(self, include=None, exclude=None): """Return a MIME bundle for display in Jupyter frontends.""" diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py index 5e6256252..1b979c189 100644 --- a/altair/utils/schemapi.py +++ b/altair/utils/schemapi.py @@ -2,6 +2,7 @@ # tools/generate_schema_wrapper.py. Do not modify directly. import collections import contextlib +import functools import inspect import json from typing import Any @@ -10,6 +11,7 @@ import numpy as np import pandas as pd +JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator # If DEBUG_MODE is True, then schema objects are converted to dict and # validated at creation time. This slows things down, particularly for @@ -40,6 +42,35 @@ def debug_mode(arg): DEBUG_MODE = original +@functools.wraps(jsonschema.validate) +def validate_jsonschema(*args, **kwargs): + # We always want to use the same jsonschema validator across the whole codebase + validator_cls = JSONSCHEMA_VALIDATOR + kwargs["cls"] = validator_cls + + removed_format_checkers = [] + try: + # The "uri-reference" checker fails for some of the Vega-Lite + # schemas as URIs in "$ref" are not encoded, + # e.g. '#/definitions/ValueDefWithCondition' would be a valid $ref in a Vega-Lite schema but + # it is not a valid URI reference due to the characters such as '<'. + # This is fine and we can disable this format check below- + for format_name in ["uri-reference"]: + try: + checker = validator_cls.FORMAT_CHECKER.checkers.pop(format_name) + removed_format_checkers.append((format_name, checker)) + except KeyError: + continue + output = jsonschema.validate(*args, **kwargs) + finally: + # Restore the original set of checkers as the jsonschema package + # might also be used by other packages + for format_name, checker in removed_format_checkers: + validator_cls.FORMAT_CHECKER.checkers[format_name] = checker + return output + + def _subclasses(cls): """Breadth-first sequence of all classes which inherit from cls.""" seen = set() @@ -155,7 +186,7 @@ class SchemaBase(object): _schema = None _rootschema = None _class_is_valid_at_instantiation = True - _validator = jsonschema.Draft7Validator + _validator = JSONSCHEMA_VALIDATOR def __init__(self, *args, **kwds): # Two valid options for initialization, which should be handled by @@ -445,9 +476,7 @@ def validate(cls, instance, schema=None): if schema is None: schema = cls._schema resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) - return jsonschema.validate( - instance, schema, cls=cls._validator, resolver=resolver - ) + return validate_jsonschema(instance, schema, resolver=resolver) @classmethod def resolve_references(cls, schema=None): @@ -466,7 +495,7 @@ def validate_property(cls, name, value, schema=None): value = _todict(value, validate=False, context={}) props = cls.resolve_references(schema or cls._schema).get("properties", {}) resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) - return jsonschema.validate(value, props.get(name, {}), resolver=resolver) + return validate_jsonschema(value, props.get(name, {}), resolver=resolver) def __dir__(self): return list(self._kwds.keys()) @@ -560,7 +589,7 @@ def from_dict( for possible_schema in schemas: resolver = jsonschema.RefResolver.from_schema(rootschema) try: - jsonschema.validate(dct, possible_schema, resolver=resolver) + validate_jsonschema(dct, possible_schema, resolver=resolver) except jsonschema.ValidationError: continue else: diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py index 7895733c5..2100efa4c 100644 --- a/tools/schemapi/schemapi.py +++ b/tools/schemapi/schemapi.py @@ -1,5 +1,6 @@ import collections import contextlib +import functools import inspect import json from typing import Any @@ -8,6 +9,7 @@ import numpy as np import pandas as pd +JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator # If DEBUG_MODE is True, then schema objects are converted to dict and # validated at creation time. This slows things down, particularly for @@ -38,6 +40,35 @@ def debug_mode(arg): DEBUG_MODE = original +@functools.wraps(jsonschema.validate) +def validate_jsonschema(*args, **kwargs): + # We always want to use the same jsonschema validator across the whole codebase + validator_cls = JSONSCHEMA_VALIDATOR + kwargs["cls"] = validator_cls + + removed_format_checkers = [] + try: + # The "uri-reference" checker fails for some of the Vega-Lite + # schemas as URIs in "$ref" are not encoded, + # e.g. '#/definitions/ValueDefWithCondition' would be a valid $ref in a Vega-Lite schema but + # it is not a valid URI reference due to the characters such as '<'. + # This is fine and we can disable this format check below- + for format_name in ["uri-reference"]: + try: + checker = validator_cls.FORMAT_CHECKER.checkers.pop(format_name) + removed_format_checkers.append((format_name, checker)) + except KeyError: + continue + output = jsonschema.validate(*args, **kwargs) + finally: + # Restore the original set of checkers as the jsonschema package + # might also be used by other packages + for format_name, checker in removed_format_checkers: + validator_cls.FORMAT_CHECKER.checkers[format_name] = checker + return output + + def _subclasses(cls): """Breadth-first sequence of all classes which inherit from cls.""" seen = set() @@ -153,7 +184,7 @@ class SchemaBase(object): _schema = None _rootschema = None _class_is_valid_at_instantiation = True - _validator = jsonschema.Draft7Validator + _validator = JSONSCHEMA_VALIDATOR def __init__(self, *args, **kwds): # Two valid options for initialization, which should be handled by @@ -443,9 +474,7 @@ def validate(cls, instance, schema=None): if schema is None: schema = cls._schema resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) - return jsonschema.validate( - instance, schema, cls=cls._validator, resolver=resolver - ) + return validate_jsonschema(instance, schema, resolver=resolver) @classmethod def resolve_references(cls, schema=None): @@ -464,7 +493,7 @@ def validate_property(cls, name, value, schema=None): value = _todict(value, validate=False, context={}) props = cls.resolve_references(schema or cls._schema).get("properties", {}) resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) - return jsonschema.validate(value, props.get(name, {}), resolver=resolver) + return validate_jsonschema(value, props.get(name, {}), resolver=resolver) def __dir__(self): return list(self._kwds.keys()) @@ -558,7 +587,7 @@ def from_dict( for possible_schema in schemas: resolver = jsonschema.RefResolver.from_schema(rootschema) try: - jsonschema.validate(dct, possible_schema, resolver=resolver) + validate_jsonschema(dct, possible_schema, resolver=resolver) except jsonschema.ValidationError: continue else: