From 28442669df2ee646c3d8e8fc18bd37c663c6d1eb Mon Sep 17 00:00:00 2001 From: k clair Date: Tue, 9 Oct 2012 12:36:56 -0700 Subject: add source files for the python jsonschema module --- jsonschema-0.6.tar.gz | Bin 0 -> 13779 bytes jsonschema-0.6/CHANGELOG.rst | 42 +++ jsonschema-0.6/COPYING | 19 ++ jsonschema-0.6/PKG-INFO | 158 +++++++++ jsonschema-0.6/README.rst | 135 ++++++++ jsonschema-0.6/jsonschema.py | 658 +++++++++++++++++++++++++++++++++++ jsonschema-0.6/setup.py | 38 +++ jsonschema-0.6/tests.py | 796 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1846 insertions(+) create mode 100644 jsonschema-0.6.tar.gz create mode 100644 jsonschema-0.6/CHANGELOG.rst create mode 100644 jsonschema-0.6/COPYING create mode 100644 jsonschema-0.6/PKG-INFO create mode 100644 jsonschema-0.6/README.rst create mode 100644 jsonschema-0.6/jsonschema.py create mode 100644 jsonschema-0.6/setup.py create mode 100644 jsonschema-0.6/tests.py diff --git a/jsonschema-0.6.tar.gz b/jsonschema-0.6.tar.gz new file mode 100644 index 0000000..51ee84b Binary files /dev/null and b/jsonschema-0.6.tar.gz differ diff --git a/jsonschema-0.6/CHANGELOG.rst b/jsonschema-0.6/CHANGELOG.rst new file mode 100644 index 0000000..ccdebf2 --- /dev/null +++ b/jsonschema-0.6/CHANGELOG.rst @@ -0,0 +1,42 @@ +v0.6 +---- + +* Bugfixes + * Issue #30 - Wrong behavior for the dependencies property validation + * Fix a miswritten test + +v0.5 +---- + +* Bugfixes + * Issue #17 - require path for error objects + * Issue #18 - multiple type validation for non-objects + + +v0.4 +---- + +* Preliminary support for programmatic access to error details (Issue #5). + There are certainly some corner cases that don't do the right thing yet, but + this works mostly. + + In order to make this happen (and also to clean things up a bit), a number + of deprecations are necessary: + * ``stop_on_error`` is deprecated in ``Validator.__init__``. Use + ``Validator.iter_errors()`` instead. + * ``number_types`` and ``string_types`` are deprecated there as well. + Use ``types={"number" : ..., "string" : ...}`` instead. + * ``meta_validate`` is also deprecated, and instead is now accepted as + an argument to ``validate``, ``iter_errors`` and ``is_valid``. + +* A bugfix or two + +v0.3 +---- + +* Default for unknown types and properties is now to *not* error (consistent + with the schema). +* Python 3 support +* Removed dependency on SecureTypes now that the hash bug has been resolved. +* "Numerous bug fixes" -- most notably, a divisibleBy error for floats and a + bunch of missing typechecks for irrelevant properties. diff --git a/jsonschema-0.6/COPYING b/jsonschema-0.6/COPYING new file mode 100644 index 0000000..d8338a3 --- /dev/null +++ b/jsonschema-0.6/COPYING @@ -0,0 +1,19 @@ +Copyright (c) 2011 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jsonschema-0.6/PKG-INFO b/jsonschema-0.6/PKG-INFO new file mode 100644 index 0000000..eca632a --- /dev/null +++ b/jsonschema-0.6/PKG-INFO @@ -0,0 +1,158 @@ +Metadata-Version: 1.1 +Name: jsonschema +Version: 0.6 +Summary: An implementation of JSON-Schema validation for Python +Home-page: http://github.com/Julian/jsonschema +Author: Julian Berman +Author-email: Julian@GrayVines.com +License: MIT/X +Description: ========== + jsonschema + ========== + + ``jsonschema`` is an implementation of JSON Schema (currently in `Draft 3 + `_) for Python (supporting + 2.6+ including Python 3). + + :: + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate({"name" : "Eggs", "price" : 34.99}, schema) + + >>> validate( + ... {"name" : "Eggs", "price" : "Invalid"}, schema + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + + + Features + -------- + + * Support for Draft 3 of the Schema with the exception of + + * ``$ref``, and ``extends`` that use ``$ref``\s + + * Lazy validation that can iteratively report *all* validation errors. + + :: + + >>> from jsonschema import Validator + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Validator() + >>> for error in sorted(v.iter_errors([2, 3, 4], schema), key=str): + ... print(error) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + + * Small and extensible + + * Programmatic querying of which properties or items failed validation. + + :: + + >>> from jsonschema import ErrorTree, Validator + >>> schema = { + ... "type" : "array", + ... "items" : {"type" : "number", "enum" : [1, 2, 3]}, + ... "minItems" : 3, + ... } + >>> instance = ["spam", 2] + >>> v = Validator() + >>> tree = ErrorTree(v.iter_errors(instance, schema)) + + >>> sorted(tree.errors) + ['minItems'] + + >>> 0 in tree + True + + >>> 1 in tree + False + + >>> sorted(tree[0].errors) + ['enum', 'type'] + + >>> print(tree[0].errors["type"].message) + 'spam' is not of type 'number' + + + Schema Versioning + ----------------- + + JSON Schema is, at the time of this writing, seemingly at Draft 3, with + preparations for Draft 4 underway. The ``Validator`` class and ``validate`` + function take a ``version`` argument that you can use to specify what version + of the Schema you are validating under. + + As of right now, Draft 3 (``jsonschema.DRAFT_3``) is the only supported + version, and the default when validating. Whether it will remain the default + version in the future when it is superceeded is undecided, so if you want to be + safe, *explicitly* declare which version to use when validating. + + + Release Notes + ------------- + + ``0.6`` fixes the behavior for the ``dependencies`` property, which was + mis-implemented. + + + Running the Test Suite + ---------------------- + + ``jsonschema`` uses the wonderful `Tox `_ for its + test suite. (It really is wonderful, if for some reason you haven't heard of + it, you really should use it for your projects). + + Assuming you have ``tox`` installed (perhaps via ``pip install tox`` or your + package manager), just run ``tox`` in the directory of your source checkout to + run ``jsonschema``'s test suite on all of the versions of Python ``jsonschema`` + supports. Note that you'll need to have all of those versions installed in + order to run the tests on each of them, otherwise ``tox`` will skip (and fail) + the tests on that version. + + + Contributing + ------------ + + I'm Julian Berman. + + ``jsonschema`` is on `GitHub `_. + + Get in touch, via GitHub or otherwise, if you've got something to contribute, + it'd be most welcome! + + You can also generally find me on Freenode (nick: ``tos9``) in various + channels, including ``#python``. + +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy diff --git a/jsonschema-0.6/README.rst b/jsonschema-0.6/README.rst new file mode 100644 index 0000000..28e0451 --- /dev/null +++ b/jsonschema-0.6/README.rst @@ -0,0 +1,135 @@ +========== +jsonschema +========== + +``jsonschema`` is an implementation of JSON Schema (currently in `Draft 3 +`_) for Python (supporting +2.6+ including Python 3). + +:: + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate({"name" : "Eggs", "price" : 34.99}, schema) + + >>> validate( + ... {"name" : "Eggs", "price" : "Invalid"}, schema + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + + +Features +-------- + +* Support for Draft 3 of the Schema with the exception of + + * ``$ref``, and ``extends`` that use ``$ref``\s + +* Lazy validation that can iteratively report *all* validation errors. + +:: + + >>> from jsonschema import Validator + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Validator() + >>> for error in sorted(v.iter_errors([2, 3, 4], schema), key=str): + ... print(error) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + +* Small and extensible + +* Programmatic querying of which properties or items failed validation. + +:: + + >>> from jsonschema import ErrorTree, Validator + >>> schema = { + ... "type" : "array", + ... "items" : {"type" : "number", "enum" : [1, 2, 3]}, + ... "minItems" : 3, + ... } + >>> instance = ["spam", 2] + >>> v = Validator() + >>> tree = ErrorTree(v.iter_errors(instance, schema)) + + >>> sorted(tree.errors) + ['minItems'] + + >>> 0 in tree + True + + >>> 1 in tree + False + + >>> sorted(tree[0].errors) + ['enum', 'type'] + + >>> print(tree[0].errors["type"].message) + 'spam' is not of type 'number' + + +Schema Versioning +----------------- + +JSON Schema is, at the time of this writing, seemingly at Draft 3, with +preparations for Draft 4 underway. The ``Validator`` class and ``validate`` +function take a ``version`` argument that you can use to specify what version +of the Schema you are validating under. + +As of right now, Draft 3 (``jsonschema.DRAFT_3``) is the only supported +version, and the default when validating. Whether it will remain the default +version in the future when it is superceeded is undecided, so if you want to be +safe, *explicitly* declare which version to use when validating. + + +Release Notes +------------- + +``0.6`` fixes the behavior for the ``dependencies`` property, which was +mis-implemented. + + +Running the Test Suite +---------------------- + +``jsonschema`` uses the wonderful `Tox `_ for its +test suite. (It really is wonderful, if for some reason you haven't heard of +it, you really should use it for your projects). + +Assuming you have ``tox`` installed (perhaps via ``pip install tox`` or your +package manager), just run ``tox`` in the directory of your source checkout to +run ``jsonschema``'s test suite on all of the versions of Python ``jsonschema`` +supports. Note that you'll need to have all of those versions installed in +order to run the tests on each of them, otherwise ``tox`` will skip (and fail) +the tests on that version. + + +Contributing +------------ + +I'm Julian Berman. + +``jsonschema`` is on `GitHub `_. + +Get in touch, via GitHub or otherwise, if you've got something to contribute, +it'd be most welcome! + +You can also generally find me on Freenode (nick: ``tos9``) in various +channels, including ``#python``. diff --git a/jsonschema-0.6/jsonschema.py b/jsonschema-0.6/jsonschema.py new file mode 100644 index 0000000..9910c84 --- /dev/null +++ b/jsonschema-0.6/jsonschema.py @@ -0,0 +1,658 @@ +""" +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 :class:`Validator` class generally attempts to be as strict as possible +under the JSON Schema specification. See its docstring for details. + +""" + +from __future__ import division, unicode_literals + +import collections +import itertools +import operator +import re +import sys +import warnings + + +PY3 = sys.version_info[0] >= 3 + +if PY3: + basestring = unicode = str + iteritems = operator.methodcaller("items") +else: + from itertools import izip as zip + iteritems = operator.methodcaller("iteritems") + + +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 + + +__version__ = "0.6" + + +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" + }, +} + +EPSILON = 10 ** -15 + + +class SchemaError(Exception): + """ + The provided schema is malformed. + + The same attributes exist for ``SchemaError``s as for ``ValidationError``s. + + """ + + validator = None + + def __init__(self, message): + super(SchemaError, self).__init__(message) + self.message = message + self.path = [] + + +class ValidationError(Exception): + """ + The instance didn't properly validate with the provided schema. + + Relevant attributes are: + * ``message`` : a human readable message explaining the error + * ``path`` : a list containing the path to the offending element (or [] + if the error happened globally) in *reverse* order (i.e. + deepest index first). + + """ + + # the failing validator will be set externally at whatever recursion level + # is immediately above the validation failure + validator = None + + def __init__(self, message): + super(ValidationError, self).__init__(message) + self.message = message + + # Any validator that recurses must append to the ValidationError's + # path (e.g., properties and items) + self.path = [] + + +class Validator(object): + """ + A JSON Schema validator. + + """ + + DEFAULT_TYPES = { + "array" : list, "boolean" : bool, "integer" : int, "null" : type(None), + "number" : (int, float), "object" : dict, "string" : basestring, + } + + def __init__( + self, version=DRAFT_3, unknown_type="skip", + unknown_property="skip", 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). + + ``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. + + ``types`` is a mapping (or iterable of 2-tuples) containing additional + types or alternate types to verify via the 'type' property. For + instance, the default types for the 'number' JSON Schema type are + ``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. + + """ + + 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()) + + def is_type(self, instance, type): + """ + Check if an ``instance`` is of the provided ``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,) + ) + + # the only thing we're careful about here is evading bool inheriting + # from int, so let's be even dirtier than usual + + elif ( + # it's not a bool, so no worries + not isinstance(instance, bool) or + + # 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 + ) + + ): + 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) + + def is_valid(self, instance, schema, meta_validate=True): + """ + Check if the ``instance`` is valid under the ``schema``. + + Returns a bool indicating whether validation succeeded. + + """ + + error = next(self.iter_errors(instance, schema, meta_validate), None) + return error is None + + def iter_errors(self, instance, schema, meta_validate=True): + """ + 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): + 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) + + for error in errors or (): + # if the validator hasn't already been set (due to recursion) + # make sure to set it + error.validator = error.validator or k + yield error + + def validate(self, *args, **kwargs): + """ + Validate an ``instance`` under the given ``schema``. + + """ + + 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) + + for type in types: + # Ouch. Brain hurts. Two paths here, either we have a schema, then + # check if the instance is valid under it + if (( + self.is_type(type, "object") and + self.is_valid(instance, type) + + # Or we have a type as a string, just check if the instance is that + # type. Also, HACK: we can reach the `or` here if skip_types is + # something other than error. If so, bail out. + + ) or ( + self.is_type(type, "string") and + (self.is_type(instance, type) or type not in self._types) + )): + return + else: + yield ValidationError( + "%r is not of type %r" % (instance, _delist(types)) + ) + + def validate_properties(self, properties, instance, schema): + if not self.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in self.iter_errors( + instance[property], subschema, meta_validate=False + ): + error.path.append(property) + yield error + elif subschema.get("required", False): + error = ValidationError( + "%r is a required property" % (property,) + ) + error.path.append(property) + error.validator = "required" + yield error + + def validate_patternProperties(self, patternProperties, instance, schema): + 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 + ): + 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", {})) + + if self.is_type(aP, "object"): + for extra in extras: + for error in self.iter_errors( + instance[extra], aP, meta_validate=False + ): + 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): + 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, meta_validate=False + ): + 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 + ): + 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 + ): + error.path.append(index) + yield error + + def validate_additionalItems(self, aI, instance, schema): + if not self.is_type(instance, "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): + 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:]) + ) + + def validate_minimum(self, minimum, instance, schema): + if not self.is_type(instance, "number"): + return + + instance = float(instance) + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + yield ValidationError( + "%r is %s the minimum of %r" % (instance, cmp, minimum) + ) + + def validate_maximum(self, maximum, instance, schema): + if not self.is_type(instance, "number"): + return + + instance = float(instance) + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + yield ValidationError( + "%r is %s the maximum of %r" % (instance, cmp, maximum) + ) + + def validate_minItems(self, mI, instance, schema): + if self.is_type(instance, "array") and len(instance) < mI: + yield ValidationError("%r is too short" % (instance,)) + + def validate_maxItems(self, mI, instance, schema): + if self.is_type(instance, "array") and len(instance) > mI: + yield ValidationError("%r is too long" % (instance,)) + + def validate_uniqueItems(self, uI, instance, schema): + if uI and self.is_type(instance, "array") and not _uniq(instance): + yield ValidationError("%r has non-unique elements" % instance) + + def validate_pattern(self, patrn, instance, schema): + if self.is_type(instance, "string") and not re.match(patrn, instance): + yield ValidationError("%r does not match %r" % (instance, patrn)) + + def validate_minLength(self, mL, instance, schema): + if self.is_type(instance, "string") and len(instance) < mL: + yield ValidationError("%r is too short" % (instance,)) + + def validate_maxLength(self, mL, instance, schema): + if self.is_type(instance, "string") and len(instance) > mL: + yield ValidationError("%r is too long" % (instance,)) + + def validate_enum(self, enums, instance, schema): + if instance not in enums: + yield ValidationError("%r is not one of %r" % (instance, enums)) + + def validate_divisibleBy(self, dB, instance, schema): + if not self.is_type(instance, "number"): + return + + if isinstance(dB, float): + mod = instance % dB + failed = (mod > EPSILON) and (dB - mod) > EPSILON + else: + failed = instance % dB + + if failed: + yield ValidationError("%r is not divisible by %r" % (instance, dB)) + + def validate_disallow(self, disallow, instance, schema): + for disallowed in _list(disallow): + if self.is_valid(instance, {"type" : [disallowed]}): + yield ValidationError( + "%r is disallowed for %r" % (disallowed, instance) + ) + + def validate_extends(self, extends, instance, schema): + if self.is_type(extends, "object"): + extends = [extends] + for subschema in extends: + for error in self.iter_errors( + instance, subschema, meta_validate=False + ): + yield error + + +for no_op in [ # handled in: + "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 ErrorTree(object): + """ + ErrorTrees make it easier to check which validations failed. + + """ + + def __init__(self, errors=()): + self.errors = {} + self._contents = collections.defaultdict(self.__class__) + + for error in errors: + container = self + for element in reversed(error.path): + container = container[element] + container.errors[error.validator] = error + + def __contains__(self, k): + return k in self._contents + + def __getitem__(self, k): + return self._contents[k] + + def __setitem__(self, k, v): + self._contents[k] = v + + def __iter__(self): + return iter(self._contents) + + def __len__(self): + child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) + return len(self.errors) + child_errors + + def __repr__(self): + return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) + + +def _extras_msg(extras): + """ + Create an error message for extra items or properties. + + """ + + if len(extras) == 1: + verb = "was" + else: + verb = "were" + return ", ".join(repr(extra) for extra in extras), verb + + +def _list(thing): + """ + Wrap ``thing`` in a list if it's a single str. + + Otherwise, return it unchanged. + + """ + + if isinstance(thing, basestring): + return [thing] + return thing + + +def _delist(thing): + """ + Unwrap ``thing`` to a single element if its a single str in a list. + + Otherwise, return it unchanged. + + """ + + if ( + isinstance(thing, list) and + len(thing) == 1 + and isinstance(thing[0], basestring) + ): + return thing[0] + return thing + + +def validate( + instance, schema, meta_validate=True, cls=Validator, *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. + + 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. + + """ + + validator = cls(*args, **kwargs) + validator.validate(instance, schema, meta_validate=meta_validate) diff --git a/jsonschema-0.6/setup.py b/jsonschema-0.6/setup.py new file mode 100644 index 0000000..7b69ae8 --- /dev/null +++ b/jsonschema-0.6/setup.py @@ -0,0 +1,38 @@ +from distutils.core import setup + +from jsonschema import __version__ + + +with open("README.rst") as readme: + long_description = readme.read() + + +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + + +setup( + name="jsonschema", + version=__version__, + py_modules=["jsonschema"], + author="Julian Berman", + author_email="Julian@GrayVines.com", + classifiers=classifiers, + description="An implementation of JSON-Schema validation for Python", + license="MIT/X", + long_description=long_description, + url="http://github.com/Julian/jsonschema", +) diff --git a/jsonschema-0.6/tests.py b/jsonschema-0.6/tests.py new file mode 100644 index 0000000..56b1481 --- /dev/null +++ b/jsonschema-0.6/tests.py @@ -0,0 +1,796 @@ +from __future__ import unicode_literals +from decimal import Decimal +from functools import wraps +import sys +import warnings + +if sys.version_info[:2] < (2, 7): # pragma: no cover + import unittest2 as unittest +else: + import unittest + + +from jsonschema import ( + PY3, SchemaError, ValidationError, ErrorTree, Validator, + iteritems, validate +) + + +if PY3: + basestring = unicode = str + + +class ParametrizedTestCase(type): + """ + A (deliberately naive & specialized) parametrized test. + + """ + + def __new__(cls, name, bases, attrs): + attr = {} + + for k, v in iteritems(attrs): + parameters = getattr(v, "_parameters", None) + + if parameters is not None: + for parameter in parameters: + parametrized_name, args = parameter[0], parameter[1:] + fn = partial(v, *args) + + names = ["test", k] + if parametrized_name: + names.append(parametrized_name) + + fn_name = "_".join(names) + if not PY3: + fn_name = fn_name.encode('utf8') + fn.__name__ = fn_name + attr[fn.__name__] = fn + else: + attr[k] = v + + if not PY3: + name = name.encode('utf8') + + return super(ParametrizedTestCase, cls).__new__(cls, name, bases, attr) + + +# Inscrutable way to create metaclasses in a Python 2/3 compatible way +# See: http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/ +ParameterizedTestCase = ParametrizedTestCase( + 'ParameterizedTestCase', (object,), {} +) + + +def parametrized(*runs): + def parametrized_test(fn): + fn._parameters = runs + return fn + return parametrized_test + + +def partial(fn, *args, **kwargs): + """ + ``functools.partial`` for methods (suitable for binding). + + """ + + @wraps(fn) + def _partial(self): + return fn(self, *args, **kwargs) + return _partial + + +def validation_test(schema=(), initkwargs=(), **kwschema): + schema = dict(schema, **kwschema) + initkwargs = dict(initkwargs) + + def _validation_test(self, expected, instance): + if expected == "valid": + validate(instance, schema, **initkwargs) + elif expected == "invalid": + with self.assertRaises(ValidationError): + validate(instance, schema, **initkwargs) + else: # pragma: no cover + raise ValueError("You spelled something wrong.") + + return _validation_test + + +class TestValidate(ParameterizedTestCase, unittest.TestCase): + integer = parametrized( + ("integer", "valid", 1), + ("number", "invalid", 1.1), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type="integer")) + + number = parametrized( + ("integer", "valid", 1), + ("number", "valid", 1.1), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type="number")) + + _string = [ + ("integer", "invalid", 1), + ("number", "invalid", 1.1), + ("unicode", "valid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + ] + + if not PY3: + # The JSON module in Python2 does not always produce unicode objects :/ + _string.append(("bytestring", "valid", b"foo")) + + string = parametrized(*_string)(validation_test(type="string")) + + object = parametrized( + ("integer", "invalid", 1), + ("number", "invalid", 1.1), + ("string", "invalid", "foo"), + ("object", "valid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type="object")) + + array = parametrized( + ("integer", "invalid", 1), + ("number", "invalid", 1.1), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "valid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type="array")) + + boolean = parametrized( + ("integer", "invalid", 1), + ("number", "invalid", 1.1), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("true", "valid", True), + ("false", "valid", False), + ("null", "invalid", None), + )(validation_test(type="boolean")) + + null = parametrized( + ("integer", "invalid", 1), + ("number", "invalid", 1.1), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "valid", None), + )(validation_test(type="null")) + + any = parametrized( + ("integer", "valid", 1), + ("number", "valid", 1.1), + ("string", "valid", "foo"), + ("object", "valid", {}), + ("array", "valid", []), + ("boolean", "valid", True), + ("null", "valid", None), + )(validation_test(type="any")) + + multiple_types = parametrized( + ("integer", "valid", 1), + ("string", "valid", "foo"), + ("number", "invalid", 1.1), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type=["integer", "string"])) + + multiple_types_schema = parametrized( + ("match", "valid", [1, 2]), + ("other_match", "valid", {"foo" : "bar"}), + ("number", "invalid", 1.1), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test(type=["array", {"type" : "object"}])) + + multiple_types_subschema = parametrized( + ("integer", "valid", 1), + ("object_right_type", "valid", {"foo" : None}), + ("object_wrong_type", "invalid", {"foo" : 1}), + ("object_another_wrong_type", "invalid", {"foo" : 1.1}), + )(validation_test( + type=["integer", {"properties" : {"foo" : {"type" : "null"}}}] + )) + + def test_multiple_types_nonobject(self): + """ + Regression test for issue #18. + + """ + validate( + [1, 2, 3], + {"type" : [{"type" : ["string"]}, {"type" : ["array", "null"]}]} + ) + + properties = parametrized( + ("", "valid", {"foo" : 1, "bar" : "baz"}), + ("extra_property", "valid", + {"foo" : 1, "bar" : "baz", "quux" : 42}), + ("invalid_type", "invalid", {"foo" : 1, "bar" : []}), + )(validation_test( + { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + )) + + patternProperties = parametrized( + ("single_match", "valid", {"foo" : 1}), + ("multiple_match", "valid", {"foo" : 1, "fah" : 2, "bar" : "baz"}), + ("single_mismatch", "invalid", {"foo" : "bar"}), + ("multiple_mismatch", "invalid", {"foo" : 1, "fah" : "bar"}), + )(validation_test(patternProperties={"f.*" : {"type" : "integer"}})) + + multiple_patternProperties = parametrized( + ("match", "valid", {"a" : 21}), + ("other_match", "valid", {"aaaa" : 18}), + ("multiple_match", "valid", {"a" : 21, "aaaa" : 18}), + ("mismatch", "invalid", {"aaa" : "bar"}), + ("other_mismatch", "invalid", {"aaaa" : 31}), + ("multiple_mismatch", "invalid", {"aaa" : "foo", "aaaa" : 32}), + )(validation_test(patternProperties={ + "a*" : {"type" : "integer"}, + "aaa*" : {"maximum" : 20}, + } + )) + + def test_additionalProperties_allowed_by_default(self): + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + validate({"foo" : 1, "bar" : "baz", "quux" : False}, schema) + + @parametrized( + ("", False), + ("schema", {"type" : "boolean"}), + ) + def additionalProperties(self, aP): + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + }, + + "additionalProperties" : aP, + } + + with self.assertRaises(ValidationError): + validate({"foo" : 1, "bar" : "baz", "quux" : "boom"}, schema) + + def test_additionalProperties_ignores_nonobjects(self): + validate(None, {"additionalProperties" : False}) + + @parametrized( + ("single_extra", {"foo" : 2}, ["'foo' was unexpected)"]), + ("multiple_extras", + dict.fromkeys(["foo", "bar", "quux"]), + ["'bar'", "'foo'", "'quux'", "were unexpected)"], + ), + ) + def additionalProperties_errorMessage(self, instance, errs): + schema = {"additionalProperties" : False} + + with self.assertRaises(ValidationError) as error: + validate(instance, schema) + + self.assertTrue(all(err in unicode(error.exception) for err in errs)) + + items = parametrized( + ("", "valid", [1, 2, 3]), + ("wrong_type", "invalid", [1, "x"]), + )(validation_test(items={"type" : "integer"})) + + items_tuple_typing = parametrized( + ("", "valid", [1, "foo"]), + ("wrong_type", "invalid", ["foo", 1]) + )(validation_test(items=[{"type" : "integer"}, {"type" : "string"}])) + + def test_additionalItems_allowed_by_default(self): + validate( + [1, "foo", False], + {"items" : [{"type" : "integer"}, {"type" : "string"}]} + ) + + additionalItems = parametrized( + ("no_additional", "valid", [1, "foo"]), + ("additional", "invalid", [1, "foo", False]), + )(validation_test({ + "items" : [{"type" : "integer"}, {"type" : "string"}], + "additionalItems" : False, + })) + + additionalItems_schema = parametrized( + ("match", "valid", [1, "foo", 3]), + ("mismatch", "invalid", [1, "foo", "bar"]), + )(validation_test({ + "items" : [{"type" : "integer"}, {"type" : "string"}], + "additionalItems" : {"type" : "integer"}, + })) + + def test_additionalItems_ignores_nonarrays(self): + validate(None, {"additionalItems" : False}) + + @parametrized( + ("single_extra", [2], "(2 was unexpected)"), + ("multiple_extras", [1, 2, 3], "(1, 2, 3 were unexpected)"), + ) + def additionalItems_errorMessage(self, instance, err): + schema = {"additionalItems" : False} + self.assertRaisesRegexp( + ValidationError, err, validate, instance, schema + ) + + @parametrized( + ("false_by_default", "valid", {}, {}), + ("false_explicit", "valid", {"required" : False}, {}), + ("one", "valid", {"required" : True}, {}), + ("other", "invalid", {}, {"required" : True}), + ("both", "invalid", {"required" : True}, {"required" : True}), + ) + def required(self, expect, foo, bar): + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + + schema["properties"]["foo"].update(foo) + schema["properties"]["bar"].update(bar) + + test = validation_test(schema) + test(self, expect, {"foo" : 1}) + + dependencies = parametrized( + ("neither", "valid", {}), + ("nondependant", "valid", {"foo" : 1}), + ("with_dependency", "valid", {"foo" : 1, "bar" : 2}), + ("missing_dependency", "invalid", {"bar" : 2}), + )(validation_test(dependencies={"bar": "foo"})) + + multiple_dependencies = parametrized( + ("neither", "valid", {}), + ("nondependants", "valid", {"foo" : 1, "bar" : 2}), + ("with_dependencies", "valid", {"foo" : 1, "bar" : 2, "quux" : 3}), + ("missing_dependency", "invalid", {"foo" : 1, "quux" : 2}), + ("missing_other_dependency", "invalid", {"bar" : 1, "quux" : 2}), + ("missing_both_dependencies", "invalid", {"quux" : 1}), + )(validation_test( + dependencies={"quux" : ["foo", "bar"]} + )) + + multiple_dependencies_subschema = parametrized( + ("", "valid", {"foo" : 1, "bar" : 2}), + ("wrong_type", "invalid", {"foo" : "quux", "bar" : 2}), + ("wrong_type_other", "invalid", {"foo" : 2, "bar" : "quux"}), + ("wrong_type_both", "invalid", {"foo" : "quux", "bar" : "quux"}), + )(validation_test(dependencies={ + "bar" : { + "properties" : { + "foo" : {"type" : "integer"}, + "bar" : {"type" : "integer"}, + }}})) + + def test_dependencies_error_message_has_single_element_not_list(self): + with self.assertRaises(ValidationError) as e: + validate({"bar" : 2}, {"dependencies" : {"bar" : "foo"}}) + self.assertNotIn("'foo']", e.exception.message) + self.assertIn("'foo'", e.exception.message) + + @parametrized( + ("", "valid", {}, 2.6), + ("fail", "invalid", {}, .6), + ("exclusiveMinimum", "valid", {"exclusiveMinimum" : True}, 1.2), + ("exclusiveMinimum_fail", "invalid", + {"exclusiveMinimum" : True}, 1.1), + ) + def minimum(self, expect, eM, instance): + eM["minimum"] = 1.1 + test = validation_test(eM) + test(self, expect, instance) + + @parametrized( + ("", "valid", {}, 2.6), + ("fail", "invalid", {}, 3.5), + ("exclusiveMaximum", "valid", {"exclusiveMaximum" : True}, 2.2), + ("exclusiveMaximum_fail", "invalid", + {"exclusiveMaximum" : True}, 3.0), + ) + def maximum(self, expect, eM, instance): + eM["maximum"] = 3.0 + test = validation_test(eM) + test(self, expect, instance) + + minItems = parametrized( + ("exact", "valid", [1]), + ("longer", "valid", [1, 2]), + ("too_short", "invalid", []), + ("ignores_strings", "valid", "a"), + )(validation_test(minItems=1)) + + maxItems = parametrized( + ("exact", "valid", [1, 2]), + ("shorter", "valid", [1]), + ("empty", "valid", []), + ("too_long", "invalid", [1, 2, 3]), + ("ignores_strings", "valid", "aaaa"), + )(validation_test(maxItems=2)) + + uniqueItems = parametrized( + ("unique", "valid", [1, 2]), + ("not_unique", "invalid", [1, 1]), + ("object_unique", "valid", [{"foo" : "bar"}, {"foo" : "baz"}]), + ("object_not_unique", "invalid", [{"foo" : "bar"}, {"foo" : "bar"}]), + ("array_unique", "valid", [["foo"], ["bar"]]), + ("array_not_unique", "invalid", [["foo"], ["foo"]]), + ("nested", "valid", [ + {"foo" : {"bar" : {"baz" : "quux"}}}, + {"foo" : {"bar" : {"baz" : "spam"}}}, + ]), + ("nested_not_unique", "invalid", [ + {"foo" : {"bar" : {"baz" : "quux"}}}, + {"foo" : {"bar" : {"baz" : "quux"}}}, + ]) + )(validation_test(uniqueItems=True)) + + pattern = parametrized( + ("match", "valid", "aaa"), + ("mismatch", "invalid", "ab"), + ("ignores_other_stuff", "valid", True), + )(validation_test(pattern="^a*$")) + + minLength = parametrized( + ("", "valid", "foo"), + ("too_short", "invalid", "f"), + ("ignores_arrays", "valid", [1]), + )(validation_test(minLength=2)) + + maxLength = parametrized( + ("", "valid", "f"), + ("too_long", "invalid", "foo"), + ("ignores_arrays", "valid", [1, 2, 3]), + )(validation_test(maxLength=2)) + + @parametrized( + ("integer", "valid", 1, [1, 2, 3]), + ("integer_fail", "invalid", 6, [1, 2, 3]), + ("string", "valid", "foo", ["foo", "bar"]), + ("string_fail", "invalid", "quux", ["foo", "bar"]), + ("bool", "valid", True, [True]), + ("bool_fail", "invalid", False, [True]), + ("object", "valid", {"foo" : "bar"}, [{"foo" : "bar"}]), + ("object_fail", "invalid", {"foo" : "bar"}, [{"foo" : "quux"}]), + ) + def enum(self, expect, instance, enum): + test = validation_test(enum=enum) + test(self, expect, instance) + + @parametrized( + ("int_by_int", "valid", 10, 2), + ("int_by_int_fail", "invalid", 7, 2), + ("number_by_number", "valid", 3.3, 1.1), + ("number_by_number_fail", "invalid", 3.5, 1.1), + ("number_by_number_small", "valid", .0075, .0001), + ("number_by_number_small_fail", "invalid", .00751, .0001), + ("number_by_number_again", "valid", 1.09, .01), + ("number_by_number_again_2", "valid", 1.89, .01), + ) + def divisibleBy(self, expect, instance, dB): + test = validation_test(divisibleBy=dB) + test(self, expect, instance) + + disallow = parametrized( + ("", "valid", "foo"), + ("disallowed", "invalid", 1), + )(validation_test(disallow="integer")) + + multiple_disallow = parametrized( + ("", "valid", "foo"), + ("mismatch", "invalid", 1), + ("other_mismatch", "invalid", True), + )(validation_test(disallow=["integer", "boolean"])) + + multiple_disallow_subschema = parametrized( + ("match", "valid", 1), + ("other_match", "valid", {"foo" : 1}), + ("mismatch", "invalid", "foo"), + ("other_mismatch", "invalid", {"foo" : "bar"}), + )(validation_test( + disallow=[ + "string", + {"type" : "object", "properties" : {"foo" : {"type" : "string"}}}, + ] + )) + + @parametrized( + ("", "valid", {"foo" : "baz", "bar" : 2}), + ("mismatch_extends", "invalid", {"foo" : "baz"}), + ("mismatch_extended", "invalid", {"bar" : 2}), + ("wrong_type", "invalid", {"foo" : "baz", "bar" : "quux"}), + ) + def extends(self, expect, instance): + schema = { + "properties" : {"bar" : {"type" : "integer", "required" : True}}, + "extends" : { + "properties" : { + "foo" : {"type" : "string", "required" : True}, + } + }, + } + + test = validation_test(**schema) + test(self, expect, instance) + + @parametrized( + ("", "valid", {"foo" : "quux", "bar" : 2, "baz" : None}), + ("mismatch_first_extends", "invalid", {"bar" : 2, "baz" : None}), + ("mismatch_second_extends", "invalid", {"foo" : "quux", "bar" : 2}), + ("mismatch_both", "invalid", {"bar" : 2}), + ) + def multiple_extends(self, expect, instance): + schema = { + "properties" : {"bar" : {"type" : "integer", "required" : True}}, + "extends" : [ + { + "properties" : { + "foo" : {"type" : "string", "required" : True}, + } + }, + { + "properties" : { + "baz" : {"type" : "null", "required" : True}, + } + }, + ], + } + + test = validation_test(**schema) + test(self, expect, instance) + + extends_simple_types = parametrized( + ("", "valid", 25), + ("mismatch_extends", "invalid", 35) + )(validation_test(minimum=20, extends={"maximum" : 30})) + + def test_iter_errors(self): + instance = [1, 2] + schema = { + "disallow" : "array", + "enum" : [["a", "b", "c"], ["d", "e", "f"]], + "minItems" : 3 + } + + if PY3: + errors = sorted([ + "'array' is disallowed for [1, 2]", + "[1, 2] is too short", + "[1, 2] is not one of [['a', 'b', 'c'], ['d', 'e', 'f']]", + ]) + else: + errors = sorted([ + "u'array' is disallowed for [1, 2]", + "[1, 2] is too short", + "[1, 2] is not one of [[u'a', u'b', u'c'], [u'd', u'e', u'f']]", + ]) + + self.assertEqual( + sorted(str(e) for e in Validator().iter_errors(instance, schema)), + errors, + ) + + def test_unknown_type_error(self): + with self.assertRaises(SchemaError): + validate(1, {"type" : "foo"}, unknown_type="error") + + def test_unknown_type_warn(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + validate(1, {"type" : "foo"}, unknown_type="warn") + self.assertEqual(len(w), 1) + + def test_unknown_type_skip(self): + validate(1, {"type" : "foo"}, unknown_type="skip") + + def test_unknown_property_error(self): + with self.assertRaises(SchemaError): + validate(1, {"foo" : "bar"}, unknown_property="error") + + def test_unknown_property_warn(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + validate(1, {"foo" : "bar"}, unknown_property="warn") + self.assertEqual(len(w), 1) + + def test_unknown_property_skip(self): + validate( + 1, + {"foo" : "foo", "type" : "integer"}, + unknown_property="skip" + ) + + decimal = parametrized( + ("integer", "valid", 1), + ("number", "valid", 1.1), + ("decimal", "valid", Decimal(1) / Decimal(8)), + ("string", "invalid", "foo"), + ("object", "invalid", {}), + ("array", "invalid", []), + ("boolean", "invalid", True), + ("null", "invalid", None), + )(validation_test( + initkwargs={"types" : {"number" : (int, float, Decimal)}}, + type="number") + ) + + # TODO: we're in need of more meta schema tests + def test_invalid_properties(self): + with self.assertRaises(SchemaError): + validate({}, {"properties": {"test": True}}) + + def test_minItems_invalid_string(self): + with self.assertRaises(SchemaError): + validate([1], {"minItems" : "1"}) # needs to be an integer + + def test_iter_errors_multiple_failures_one_validator(self): + instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"} + schema = { + "properties" : { + "foo" : {"type" : "string"}, + "bar" : {"minItems" : 2}, + "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]}, + } + } + + errors = list(Validator().iter_errors(instance, schema)) + self.assertEqual(len(errors), 4) + + +class TestValidationErrorDetails(unittest.TestCase): + # TODO: These really need unit tests for each individual validator, rather + # than just these higher level tests. + def test_single_nesting(self): + instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"} + schema = { + "properties" : { + "foo" : {"type" : "string"}, + "bar" : {"minItems" : 2}, + "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]}, + } + } + + errors = Validator().iter_errors(instance, schema) + e1, e2, e3, e4 = sorted_errors(errors) + + self.assertEqual(e1.path, ["bar"]) + self.assertEqual(e2.path, ["baz"]) + self.assertEqual(e3.path, ["baz"]) + self.assertEqual(e4.path, ["foo"]) + + self.assertEqual(e1.validator, "minItems") + self.assertEqual(e2.validator, "enum") + self.assertEqual(e3.validator, "maximum") + self.assertEqual(e4.validator, "type") + + def test_multiple_nesting(self): + instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"] + schema = { + "type" : "string", + "items" : { + "type" : ["string", "object"], + "properties" : { + "foo" : {"enum" : [1, 3]}, + "bar" : { + "type" : "array", + "properties" : { + "bar" : {"required" : True}, + "baz" : {"minItems" : 2}, + } + } + } + } + } + + errors = Validator().iter_errors(instance, schema) + e1, e2, e3, e4, e5, e6 = sorted_errors(errors) + + self.assertEqual(e1.path, []) + self.assertEqual(e2.path, [0]) + self.assertEqual(e3.path, ["bar", 1]) + self.assertEqual(e4.path, ["bar", "bar", 1]) + self.assertEqual(e5.path, ["baz", "bar", 1]) + self.assertEqual(e6.path, ["foo", 1]) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "type") + self.assertEqual(e3.validator, "type") + self.assertEqual(e4.validator, "required") + self.assertEqual(e5.validator, "minItems") + self.assertEqual(e6.validator, "enum") + + +class TestErrorTree(unittest.TestCase): + def test_tree(self): + instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"] + schema = { + "type" : "string", + "items" : { + "type" : ["string", "object"], + "properties" : { + "foo" : {"enum" : [1, 3]}, + "bar" : { + "type" : "array", + "properties" : { + "bar" : {"required" : True}, + "baz" : {"minItems" : 2}, + } + } + } + } + } + + errors = sorted_errors(Validator().iter_errors(instance, schema)) + e1, e2, e3, e4, e5, e6 = errors + tree = ErrorTree(errors) + + self.assertEqual(len(tree), 6) + + self.assertIn(0, tree) + self.assertIn(1, tree) + self.assertIn("bar", tree[1]) + self.assertIn("foo", tree[1]) + self.assertIn("baz", tree[1]["bar"]) + + self.assertEqual(tree.errors["type"], e1) + self.assertEqual(tree[0].errors["type"], e2) + self.assertEqual(tree[1]["bar"].errors["type"], e3) + self.assertEqual(tree[1]["bar"]["bar"].errors["required"], e4) + self.assertEqual(tree[1]["bar"]["baz"].errors["minItems"], e5) + self.assertEqual(tree[1]["foo"].errors["enum"], e6) + + +class TestIgnorePropertiesForIrrelevantTypes(unittest.TestCase): + def test_minimum(self): + validate("x", {"type": ["string", "number"], "minimum": 10}) + + def test_maximum(self): + validate("x", {"type": ["string", "number"], "maximum": 10}) + + def test_properties(self): + validate(1, {"type": ["integer", "object"], "properties": {"x": {}}}) + + def test_items(self): + validate( + 1, {"type": ["integer", "array"], "items": {"type": "string"}} + ) + + def test_divisibleBy(self): + validate("x", {"type": ["integer", "string"], "divisibleBy": 10}) + + +def sorted_errors(errors): + return sorted(errors, key=lambda e : [str(err) for err in e.path]) -- cgit v1.2.3