diff --git a/IPython/external/jsonschema/VERSION b/IPython/external/jsonschema/VERSION new file mode 100644 index 0000000..8b610fd --- /dev/null +++ b/IPython/external/jsonschema/VERSION @@ -0,0 +1,3 @@ +_jsonschema from commit : +3ddd80543bd6da56eeea84b2f364febaeadf31b9 + diff --git a/IPython/external/jsonschema/_jsonschema.py b/IPython/external/jsonschema/_jsonschema.py index c7d662b..bb5a97c 100755 --- a/IPython/external/jsonschema/_jsonschema.py +++ b/IPython/external/jsonschema/_jsonschema.py @@ -1,12 +1,11 @@ """ An implementation of JSON Schema for Python -The main functionality is provided by the :class:`Validator` class, with the -:function:`validate` function being the most common way to quickly create a -:class:`Validator` object and validate an instance with a given schema. +The main functionality is provided by the validator classes for each of the +supported JSON Schema versions. -The :class:`Validator` class generally attempts to be as strict as possible -under the JSON Schema specification. See its docstring for details. +Most commonly, the :function:`validate` function is the quickest way to simply +validate a given instance under a schema, and will create a validator for you. """ @@ -20,131 +19,33 @@ import sys import warnings +__version__ = "0.7" + +FLOAT_TOLERANCE = 10 ** -15 PY3 = sys.version_info[0] >= 3 if PY3: basestring = unicode = str iteritems = operator.methodcaller("items") + from urllib.parse import unquote else: from itertools import izip as zip iteritems = operator.methodcaller("iteritems") + from urllib import unquote -def _uniq(container): +class UnknownType(Exception): """ - Check if all of a container's elements are unique. - - Successively tries first to rely that the elements are hashable, then - falls back on them being sortable, and finally falls back on brute - force. + An unknown type was given. """ - try: - return len(set(container)) == len(container) - except TypeError: - try: - sort = sorted(container) - sliced = itertools.islice(container, 1, None) - for i, j in zip(container, sliced): - if i == j: - return False - except (NotImplementedError, TypeError): - seen = [] - for e in container: - if e in seen: - return False - seen.append(e) - return True - - -__version__ = "0.5" - - -DRAFT_3 = { - "$schema" : "http://json-schema.org/draft-03/schema#", - "id" : "http://json-schema.org/draft-03/schema#", - "type" : "object", - "properties" : { - "type" : { - "type" : ["string", "array"], - "items" : {"type" : ["string", {"$ref" : "#"}]}, - "uniqueItems" : True, - "default" : "any" - }, - "properties" : { - "type" : "object", - "additionalProperties" : {"$ref" : "#", "type": "object"}, - "default" : {} - }, - "patternProperties" : { - "type" : "object", - "additionalProperties" : {"$ref" : "#"}, - "default" : {} - }, - "additionalProperties" : { - "type" : [{"$ref" : "#"}, "boolean"], "default" : {} - }, - "items" : { - "type" : [{"$ref" : "#"}, "array"], - "items" : {"$ref" : "#"}, - "default" : {} - }, - "additionalItems" : { - "type" : [{"$ref" : "#"}, "boolean"], "default" : {} - }, - "required" : {"type" : "boolean", "default" : False}, - "dependencies" : { - "type" : ["string", "array", "object"], - "additionalProperties" : { - "type" : ["string", "array", {"$ref" : "#"}], - "items" : {"type" : "string"} - }, - "default" : {} - }, - "minimum" : {"type" : "number"}, - "maximum" : {"type" : "number"}, - "exclusiveMinimum" : {"type" : "boolean", "default" : False}, - "exclusiveMaximum" : {"type" : "boolean", "default" : False}, - "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0}, - "maxItems" : {"type" : "integer", "minimum" : 0}, - "uniqueItems" : {"type" : "boolean", "default" : False}, - "pattern" : {"type" : "string", "format" : "regex"}, - "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0}, - "maxLength" : {"type" : "integer"}, - "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True}, - "default" : {"type" : "any"}, - "title" : {"type" : "string"}, - "description" : {"type" : "string"}, - "format" : {"type" : "string"}, - "maxDecimal" : {"type" : "number", "minimum" : 0}, - "divisibleBy" : { - "type" : "number", - "minimum" : 0, - "exclusiveMinimum" : True, - "default" : 1 - }, - "disallow" : { - "type" : ["string", "array"], - "items" : {"type" : ["string", {"$ref" : "#"}]}, - "uniqueItems" : True - }, - "extends" : { - "type" : [{"$ref" : "#"}, "array"], - "items" : {"$ref" : "#"}, - "default" : {} - }, - "id" : {"type" : "string", "format" : "uri"}, - "$ref" : {"type" : "string", "format" : "uri"}, - "$schema" : {"type" : "string", "format" : "uri"}, - }, - "dependencies" : { - "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum" - }, -} +class InvalidRef(Exception): + """ + An invalid reference was given. -EPSILON = 10 ** -15 + """ class SchemaError(Exception): @@ -188,9 +89,9 @@ class ValidationError(Exception): self.path = [] -class Validator(object): +class Draft3Validator(object): """ - A JSON Schema validator. + A validator for JSON Schema draft 3. """ @@ -199,23 +100,12 @@ class Validator(object): "number" : (int, float), "object" : dict, "string" : basestring, } - def __init__( - self, version=DRAFT_3, unknown_type="skip", - unknown_property="skip", types=(), - ): + def __init__(self, schema, types=()): """ - Initialize a Validator. - - ``version`` specifies which version of the JSON Schema specification to - validate with. Currently only draft-03 is supported (and is the - default). + Initialize a validator. - ``unknown_type`` and ``unknown_property`` control what to do when an - unknown type (resp. property) is encountered. By default, the - metaschema is respected (which e.g. for draft 3 allows a schema to have - additional properties), but if for some reason you want to modify this - behavior, you can do so without needing to modify the metaschema by - passing ``"error"`` or ``"warn"`` to these arguments. + ``schema`` should be a *valid* JSON Schema object already converted to + a native Python object (typically a dict via ``json.load``). ``types`` is a mapping (or iterable of 2-tuples) containing additional types or alternate types to verify via the 'type' property. For @@ -223,100 +113,76 @@ class Validator(object): ``int`` and ``float``. To override this behavior (e.g. for also allowing ``decimal.Decimal``), pass ``types={"number" : (int, float, decimal.Decimal)} *including* the default types if so desired, which - are fairly obvious but can be accessed via ``Validator.DEFAULT_TYPES`` - if necessary. + are fairly obvious but can be accessed via the ``DEFAULT_TYPES`` + attribute on this class if necessary. """ - self._unknown_type = unknown_type - self._unknown_property = unknown_property - self._version = version - self._types = dict(self.DEFAULT_TYPES) self._types.update(types) self._types["any"] = tuple(self._types.values()) + self.schema = schema + def is_type(self, instance, type): """ - Check if an ``instance`` is of the provided ``type``. + Check if an ``instance`` is of the provided (JSON Schema) ``type``. """ - py_type = self._types.get(type) - - if py_type is None: - return self.schema_error( - self._unknown_type, "%r is not a known type" % (type,) - ) + if type not in self._types: + raise UnknownType(type) + type = self._types[type] - # the only thing we're careful about here is evading bool inheriting - # from int, so let's be even dirtier than usual + # bool inherits from int, so ensure bools aren't reported as integers + if isinstance(instance, bool): + type = _flatten(type) + if int in type and bool not in type: + return False + return isinstance(instance, type) - elif ( - # it's not a bool, so no worries - not isinstance(instance, bool) or + def is_valid(self, instance, _schema=None): + """ + Check if the ``instance`` is valid under the current schema. - # it is a bool, but we're checking for a bool, so no worries - ( - py_type is bool or - isinstance(py_type, tuple) and bool in py_type - ) + Returns a bool indicating whether validation succeeded. - ): - return isinstance(instance, py_type) + """ - def schema_error(self, level, msg): - if level == "skip": - return - elif level == "warn": - warnings.warn(msg) - else: - raise SchemaError(msg) + error = next(self.iter_errors(instance, _schema), None) + return error is None - def is_valid(self, instance, schema, meta_validate=True): + @classmethod + def check_schema(cls, schema): """ - Check if the ``instance`` is valid under the ``schema``. - - Returns a bool indicating whether validation succeeded. + Validate a ``schema`` against the meta-schema to see if it is valid. """ - error = next(self.iter_errors(instance, schema, meta_validate), None) - return error is None + for error in cls(cls.META_SCHEMA).iter_errors(schema): + s = SchemaError(error.message) + s.path = error.path + s.validator = error.validator + # I think we're safer raising these always, not yielding them + raise s - def iter_errors(self, instance, schema, meta_validate=True): + def iter_errors(self, instance, _schema=None): """ Lazily yield each of the errors in the given ``instance``. - If you are unsure whether your schema itself is valid, - ``meta_validate`` will first validate that the schema is valid before - attempting to validate the instance. ``meta_validate`` is ``True`` by - default, since setting it to ``False`` can lead to confusing error - messages with an invalid schema. If you're sure your schema is in fact - valid, or don't care, feel free to set this to ``False``. The meta - validation will be done using the appropriate ``version``. - """ - if meta_validate: - for error in self.iter_errors( - schema, self._version, meta_validate=False - ): - s = SchemaError(error.message) - s.path = error.path - s.validator = error.validator - # I think we're safer raising these always, not yielding them - raise s - - for k, v in iteritems(schema): + if _schema is None: + _schema = self.schema + + for k, v in iteritems(_schema): validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) if validator is None: - errors = self.unknown_property(k, instance, schema) - else: - errors = validator(v, instance, schema) + continue - for error in errors or (): + errors = validator(v, instance, _schema) or () + for error in errors: # if the validator hasn't already been set (due to recursion) # make sure to set it error.validator = error.validator or k @@ -331,12 +197,6 @@ class Validator(object): for error in self.iter_errors(*args, **kwargs): raise error - def unknown_property(self, property, instance, schema): - self.schema_error( - self._unknown_property, - "%r is not a known schema property" % (property,) - ) - def validate_type(self, types, instance, schema): types = _list(types) @@ -357,9 +217,7 @@ class Validator(object): )): return else: - yield ValidationError( - "%r is not of type %r" % (instance, _delist(types)) - ) + yield ValidationError(_types_msg(instance, types)) def validate_properties(self, properties, instance, schema): if not self.is_type(instance, "object"): @@ -367,22 +225,7 @@ class Validator(object): for property, subschema in iteritems(properties): if property in instance: - dependencies = _list(subschema.get("dependencies", [])) - if self.is_type(dependencies, "object"): - for error in self.iter_errors( - instance, dependencies, meta_validate=False - ): - yield error - else: - for dependency in dependencies: - if dependency not in instance: - yield ValidationError( - "%r is a dependency of %r" % (dependency, property) - ) - - for error in self.iter_errors( - instance[property], subschema, meta_validate=False - ): + for error in self.iter_errors(instance[property], subschema): error.path.append(property) yield error elif subschema.get("required", False): @@ -397,59 +240,71 @@ class Validator(object): for pattern, subschema in iteritems(patternProperties): for k, v in iteritems(instance): if re.match(pattern, k): - for error in self.iter_errors( - v, subschema, meta_validate=False - ): + for error in self.iter_errors(v, subschema): yield error def validate_additionalProperties(self, aP, instance, schema): if not self.is_type(instance, "object"): return - # no viewkeys in <2.7, and pypy seems to fail on vk - vk anyhow, so... - extras = set(instance) - set(schema.get("properties", {})) + extras = set(_find_additional_properties(instance, schema)) if self.is_type(aP, "object"): for extra in extras: - for error in self.iter_errors( - instance[extra], aP, meta_validate=False - ): + for error in self.iter_errors(instance[extra], aP): yield error elif not aP and extras: error = "Additional properties are not allowed (%s %s unexpected)" yield ValidationError(error % _extras_msg(extras)) + def validate_dependencies(self, dependencies, instance, schema): + if not self.is_type(instance, "object"): + return + + for property, dependency in iteritems(dependencies): + if property not in instance: + continue + + if self.is_type(dependency, "object"): + for error in self.iter_errors(instance, dependency): + yield error + else: + dependencies = _list(dependency) + for dependency in dependencies: + if dependency not in instance: + yield ValidationError( + "%r is a dependency of %r" % (dependency, property) + ) + def validate_items(self, items, instance, schema): if not self.is_type(instance, "array"): return if self.is_type(items, "object"): for index, item in enumerate(instance): - for error in self.iter_errors( - item, items, meta_validate=False - ): + for error in self.iter_errors(item, items): error.path.append(index) yield error else: for (index, item), subschema in zip(enumerate(instance), items): - for error in self.iter_errors( - item, subschema, meta_validate=False - ): + for error in self.iter_errors(item, subschema): error.path.append(index) yield error def validate_additionalItems(self, aI, instance, schema): if not self.is_type(instance, "array"): return + if not self.is_type(schema.get("items"), "array"): + return if self.is_type(aI, "object"): for item in instance[len(schema):]: - for error in self.iter_errors(item, aI, meta_validate=False): + for error in self.iter_errors(item, aI): yield error elif not aI and len(instance) > len(schema.get("items", [])): error = "Additional items are not allowed (%s %s unexpected)" yield ValidationError( - error % _extras_msg(instance[len(schema) - 1:]) + error % _extras_msg(instance[len(schema.get("items", [])):]) ) def validate_minimum(self, minimum, instance, schema): @@ -520,7 +375,7 @@ class Validator(object): if isinstance(dB, float): mod = instance % dB - failed = (mod > EPSILON) and (dB - mod) > EPSILON + failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE else: failed = instance % dB @@ -538,20 +393,119 @@ class Validator(object): if self.is_type(extends, "object"): extends = [extends] for subschema in extends: - for error in self.iter_errors( - instance, subschema, meta_validate=False - ): + for error in self.iter_errors(instance, subschema): yield error + def validate_ref(self, ref, instance, schema): + if ref != "#" and not ref.startswith("#/"): + warnings.warn("jsonschema only supports json-pointer $refs") + return + + resolved = resolve_json_pointer(self.schema, ref) + for error in self.iter_errors(instance, resolved): + yield error + + +Draft3Validator.META_SCHEMA = { + "$schema" : "http://json-schema.org/draft-03/schema#", + "id" : "http://json-schema.org/draft-03/schema#", + "type" : "object", + + "properties" : { + "type" : { + "type" : ["string", "array"], + "items" : {"type" : ["string", {"$ref" : "#"}]}, + "uniqueItems" : True, + "default" : "any" + }, + "properties" : { + "type" : "object", + "additionalProperties" : {"$ref" : "#", "type": "object"}, + "default" : {} + }, + "patternProperties" : { + "type" : "object", + "additionalProperties" : {"$ref" : "#"}, + "default" : {} + }, + "additionalProperties" : { + "type" : [{"$ref" : "#"}, "boolean"], "default" : {} + }, + "items" : { + "type" : [{"$ref" : "#"}, "array"], + "items" : {"$ref" : "#"}, + "default" : {} + }, + "additionalItems" : { + "type" : [{"$ref" : "#"}, "boolean"], "default" : {} + }, + "required" : {"type" : "boolean", "default" : False}, + "dependencies" : { + "type" : ["string", "array", "object"], + "additionalProperties" : { + "type" : ["string", "array", {"$ref" : "#"}], + "items" : {"type" : "string"} + }, + "default" : {} + }, + "minimum" : {"type" : "number"}, + "maximum" : {"type" : "number"}, + "exclusiveMinimum" : {"type" : "boolean", "default" : False}, + "exclusiveMaximum" : {"type" : "boolean", "default" : False}, + "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0}, + "maxItems" : {"type" : "integer", "minimum" : 0}, + "uniqueItems" : {"type" : "boolean", "default" : False}, + "pattern" : {"type" : "string", "format" : "regex"}, + "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0}, + "maxLength" : {"type" : "integer"}, + "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True}, + "default" : {"type" : "any"}, + "title" : {"type" : "string"}, + "description" : {"type" : "string"}, + "format" : {"type" : "string"}, + "maxDecimal" : {"type" : "number", "minimum" : 0}, + "divisibleBy" : { + "type" : "number", + "minimum" : 0, + "exclusiveMinimum" : True, + "default" : 1 + }, + "disallow" : { + "type" : ["string", "array"], + "items" : {"type" : ["string", {"$ref" : "#"}]}, + "uniqueItems" : True + }, + "extends" : { + "type" : [{"$ref" : "#"}, "array"], + "items" : {"$ref" : "#"}, + "default" : {} + }, + "id" : {"type" : "string", "format" : "uri"}, + "$ref" : {"type" : "string", "format" : "uri"}, + "$schema" : {"type" : "string", "format" : "uri"}, + }, + "dependencies" : { + "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum" + }, +} + -for no_op in [ # handled in: - "dependencies", "required", # properties - "exclusiveMinimum", "exclusiveMaximum", # min*/max* - "default", "description", "format", "id", # no validation needed - "links", "name", "title", - "ref", "schema", # not yet supported -]: - setattr(Validator, "validate_" + no_op, lambda *args, **kwargs : None) +class Validator(Draft3Validator): + """ + Deprecated: Use :class:`Draft3Validator` instead. + + """ + + def __init__( + self, version=None, unknown_type="skip", unknown_property="skip", + *args, **kwargs + ): + super(Validator, self).__init__({}, *args, **kwargs) + warnings.warn( + "Validator is deprecated and will be removed. " + "Use Draft3Validator instead.", + DeprecationWarning, stacklevel=2, + ) class ErrorTree(object): @@ -590,6 +544,51 @@ class ErrorTree(object): return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) +def resolve_json_pointer(schema, ref): + """ + Resolve a local reference ``ref`` within the given root ``schema``. + + ``ref`` should be a local ref whose ``#`` is still present. + + """ + + if ref == "#": + return schema + + parts = ref.lstrip("#/").split("/") + + parts = map(unquote, parts) + parts = [part.replace('~1', '/').replace('~0', '~') for part in parts] + + try: + for part in parts: + schema = schema[part] + except KeyError: + raise InvalidRef("Unresolvable json-pointer %r" % ref) + else: + return schema + + +def _find_additional_properties(instance, schema): + """ + Return the set of additional properties for the given ``instance``. + + Weeds out properties that should have been validated by ``properties`` and + / or ``patternProperties``. + + Assumes ``instance`` is dict-like already. + + """ + + properties = schema.get("properties", {}) + patterns = "|".join(schema.get("patternProperties", {})) + for property in instance: + if property not in properties: + if patterns and re.search(patterns, property): + continue + yield property + + def _extras_msg(extras): """ Create an error message for extra items or properties. @@ -603,6 +602,49 @@ def _extras_msg(extras): return ", ".join(repr(extra) for extra in extras), verb +def _types_msg(instance, types): + """ + Create an error message for a failure to match the given types. + + If the ``instance`` is an object and contains a ``name`` property, it will + be considered to be a description of that object and used as its type. + + Otherwise the message is simply the reprs of the given ``types``. + + """ + + reprs = [] + for type in types: + try: + reprs.append(repr(type["name"])) + except Exception: + reprs.append(repr(type)) + return "%r is not of type %s" % (instance, ", ".join(reprs)) + + +def _flatten(suitable_for_isinstance): + """ + isinstance() can accept a bunch of really annoying different types: + * a single type + * a tuple of types + * an arbitrary nested tree of tuples + + Return a flattened tuple of the given argument. + + """ + + types = set() + + if not isinstance(suitable_for_isinstance, tuple): + suitable_for_isinstance = (suitable_for_isinstance,) + for thing in suitable_for_isinstance: + if isinstance(thing, tuple): + types.update(_flatten(thing)) + else: + types.add(thing) + return tuple(types) + + def _list(thing): """ Wrap ``thing`` in a list if it's a single str. @@ -633,21 +675,60 @@ def _delist(thing): return thing -def validate( - instance, schema, meta_validate=True, cls=Validator, *args, **kwargs -): +def _uniq(container): + """ + Check if all of a container's elements are unique. + + Successively tries first to rely that the elements are hashable, then + falls back on them being sortable, and finally falls back on brute + force. + + """ + + try: + return len(set(container)) == len(container) + except TypeError: + try: + sort = sorted(container) + sliced = itertools.islice(container, 1, None) + for i, j in zip(container, sliced): + if i == j: + return False + except (NotImplementedError, TypeError): + seen = [] + for e in container: + if e in seen: + return False + seen.append(e) + return True + + +def validate(instance, schema, cls=Draft3Validator, *args, **kwargs): """ Validate an ``instance`` under the given ``schema``. - By default, the :class:`Validator` class from this module is used to - perform the validation. To use another validator, pass it into the ``cls`` - argument. + First verifies that the provided schema is itself valid, since not doing so + can lead to less obvious failures when validating. If you know it is or + don't care, use ``YourValidator(schema).validate(instance)`` directly + instead (e.g. ``Draft3Validator``). - Any other provided positional and keyword arguments will be provided to the - ``cls``. See the :class:`Validator` class' docstring for details on the - arguments it accepts. + ``cls`` is a validator class that will be used to validate the instance. + By default this is a draft 3 validator. Any other provided positional and + keyword arguments will be provided to this class when constructing a + validator. """ - validator = cls(*args, **kwargs) - validator.validate(instance, schema, meta_validate=meta_validate) + + meta_validate = kwargs.pop("meta_validate", None) + + if meta_validate is not None: + warnings.warn( + "meta_validate is deprecated and will be removed. If you do not " + "want to validate a schema, use Draft3Validator.validate instead.", + DeprecationWarning, stacklevel=2, + ) + + if meta_validate is not False: # yes this is needed since True was default + cls.check_schema(schema) + cls(schema, *args, **kwargs).validate(instance) diff --git a/IPython/nbformat/v3/validator.py b/IPython/nbformat/v3/validator.py index 06eac96..0073b0b 100755 --- a/IPython/nbformat/v3/validator.py +++ b/IPython/nbformat/v3/validator.py @@ -1,19 +1,19 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -from IPython.external.jsonschema import Validator, validate, ValidationError +from IPython.external.jsonschema import Draft3Validator, validate, ValidationError import IPython.external.jsonpointer as jsonpointer import argparse import traceback import json -v = Validator(); def nbvalidate(nbjson, schema='v3.withref.json', key=None,verbose=True): v3schema = resolve_ref(json.load(open(schema,'r'))) if key : v3schema = jsonpointer.resolve_pointer(v3schema,key) errors = 0 - for error in v.iter_errors(nbjson, v3schema): + v = Draft3Validator(v3schema); + for error in v.iter_errors(nbjson): errors = errors + 1 if verbose: print(error)