diff options
-rw-r--r-- | .gitignore | 25 | ||||
-rw-r--r-- | .travis.yml | 13 | ||||
-rw-r--r-- | CHANGELOG.rst | 114 | ||||
-rw-r--r-- | COPYING | 19 | ||||
-rw-r--r-- | MANIFEST.in | 4 | ||||
-rw-r--r-- | README.rst | 106 | ||||
-rw-r--r-- | docs/Makefile | 153 | ||||
-rw-r--r-- | docs/conf.py | 236 | ||||
-rw-r--r-- | docs/creating.rst | 91 | ||||
-rw-r--r-- | docs/doc-requirements.txt | 1 | ||||
-rw-r--r-- | docs/errors.rst | 303 | ||||
-rw-r--r-- | docs/faq.rst | 94 | ||||
-rw-r--r-- | docs/index.rst | 52 | ||||
-rw-r--r-- | docs/jsonschema_role.py | 123 | ||||
-rw-r--r-- | docs/make.bat | 190 | ||||
-rw-r--r-- | docs/references.rst | 13 | ||||
-rw-r--r-- | docs/validate.rst | 264 | ||||
-rw-r--r-- | json/.gitignore | 1 | ||||
-rw-r--r-- | json/.travis.yml | 4 | ||||
-rw-r--r-- | json/LICENSE (renamed from LICENSE) | 0 | ||||
-rw-r--r-- | json/README.md (renamed from README.md) | 0 | ||||
-rwxr-xr-x | json/bin/jsonschema_suite (renamed from bin/jsonschema_suite) | 0 | ||||
-rw-r--r-- | json/remotes/folder/folderInteger.json (renamed from remotes/folder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/integer.json (renamed from remotes/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/subSchemas.json (renamed from remotes/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalItems.json (renamed from tests/draft3/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalProperties.json (renamed from tests/draft3/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/dependencies.json (renamed from tests/draft3/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/disallow.json (renamed from tests/draft3/disallow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/divisibleBy.json (renamed from tests/draft3/divisibleBy.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/enum.json (renamed from tests/draft3/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/extends.json (renamed from tests/draft3/extends.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/items.json (renamed from tests/draft3/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxItems.json (renamed from tests/draft3/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxLength.json (renamed from tests/draft3/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maximum.json (renamed from tests/draft3/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minItems.json (renamed from tests/draft3/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minLength.json (renamed from tests/draft3/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minimum.json (renamed from tests/draft3/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/bignum.json (renamed from tests/draft3/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format.json (renamed from tests/draft3/optional/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/jsregex.json (renamed from tests/draft3/optional/jsregex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/zeroTerminatedFloats.json (renamed from tests/draft3/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/pattern.json (renamed from tests/draft3/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/patternProperties.json (renamed from tests/draft3/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/properties.json (renamed from tests/draft3/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/ref.json (renamed from tests/draft3/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/refRemote.json (renamed from tests/draft3/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/required.json (renamed from tests/draft3/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/type.json (renamed from tests/draft3/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/uniqueItems.json (renamed from tests/draft3/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalItems.json (renamed from tests/draft4/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalProperties.json (renamed from tests/draft4/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/allOf.json (renamed from tests/draft4/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/anyOf.json (renamed from tests/draft4/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/definitions.json (renamed from tests/draft4/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/dependencies.json (renamed from tests/draft4/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/enum.json (renamed from tests/draft4/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/items.json (renamed from tests/draft4/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxItems.json (renamed from tests/draft4/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxLength.json (renamed from tests/draft4/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxProperties.json (renamed from tests/draft4/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maximum.json (renamed from tests/draft4/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minItems.json (renamed from tests/draft4/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minLength.json (renamed from tests/draft4/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minProperties.json (renamed from tests/draft4/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minimum.json (renamed from tests/draft4/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/multipleOf.json (renamed from tests/draft4/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/not.json (renamed from tests/draft4/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/oneOf.json (renamed from tests/draft4/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/bignum.json (renamed from tests/draft4/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format.json (renamed from tests/draft4/optional/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/zeroTerminatedFloats.json (renamed from tests/draft4/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/pattern.json (renamed from tests/draft4/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/patternProperties.json (renamed from tests/draft4/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/properties.json (renamed from tests/draft4/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/ref.json (renamed from tests/draft4/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/refRemote.json (renamed from tests/draft4/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/required.json (renamed from tests/draft4/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/type.json (renamed from tests/draft4/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/uniqueItems.json (renamed from tests/draft4/uniqueItems.json) | 0 | ||||
-rw-r--r-- | jsonschema/__init__.py | 26 | ||||
-rw-r--r-- | jsonschema/_format.py | 206 | ||||
-rw-r--r-- | jsonschema/_utils.py | 217 | ||||
-rw-r--r-- | jsonschema/_validators.py | 363 | ||||
-rw-r--r-- | jsonschema/compat.py | 51 | ||||
-rw-r--r-- | jsonschema/exceptions.py | 112 | ||||
-rw-r--r-- | jsonschema/schemas/draft3.json | 201 | ||||
-rw-r--r-- | jsonschema/schemas/draft4.json | 221 | ||||
-rw-r--r-- | jsonschema/tests/__init__.py | 0 | ||||
-rw-r--r-- | jsonschema/tests/compat.py | 15 | ||||
-rw-r--r-- | jsonschema/tests/test_format.py | 63 | ||||
-rw-r--r-- | jsonschema/tests/test_jsonschema_test_suite.py | 249 | ||||
-rw-r--r-- | jsonschema/tests/test_validators.py | 847 | ||||
-rw-r--r-- | jsonschema/validators.py | 508 | ||||
-rwxr-xr-x | perftest | 46 | ||||
-rw-r--r-- | setup.py | 40 | ||||
-rw-r--r-- | tox.ini | 62 |
98 files changed, 5030 insertions, 3 deletions
@@ -1 +1,26 @@ +.DS_Store +.idea + +*.pyc +*.pyo + +*.egg-info +_build +build +dist +MANIFEST + +.coverage +.coveragerc +coverage +htmlcov + +_cache +_static +_templates + +_trial_temp + +.tox + TODO diff --git a/.travis.yml b/.travis.yml index deecd61..e283cb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,11 @@ language: python -python: "2.7" -install: pip install jsonschema -script: bin/jsonschema_suite check +python: + - "pypy" + - "2.7" + - "3.2" + - "3.3" +install: + - python setup.py -q install +script: + - py.test --tb=native jsonschema + - python -m doctest README.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..6408a7c --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,114 @@ +v2.0.0 +------ + +* Added ``create`` and ``extend`` to ``jsonschema.validators`` +* Removed ``ValidatorMixin`` +* Fixed array indices ref resolution (#95) +* Fixed unknown scheme defragmenting and handling (#102) + + +v1.3.0 +------ + +* Better error tracebacks (#83) +* Raise exceptions in ``ErrorTree``\s for keys not in the instance (#92) +* __cause__ (#93) + + +v1.2.0 +------ + +* More attributes for ValidationError (#86) +* Added ``ValidatorMixin.descend`` +* Fixed bad ``RefResolutionError`` message (#82) + + +v1.1.0 +------ + +* Canonicalize URIs (#70) +* Allow attaching exceptions to ``format`` errors (#77) + + +v1.0.0 +------ + +* Support for Draft 4 +* Support for format +* Longs are ints too! +* Fixed a number of issues with ``$ref`` support (#66) +* Draft4Validator is now the default +* ``ValidationError.path`` is now in sequential order +* Added ``ValidatorMixin`` + + +v0.8.0 +------ + +* Full support for JSON References +* ``validates`` for registering new validators +* Documentation +* Bugfixes + + * uniqueItems not so unique (#34) + * Improper any (#47) + + +v0.7 +---- + +* Partial support for (JSON Pointer) ``$ref`` +* Deprecations + + * ``Validator`` is replaced by ``Draft3Validator`` with a slightly different + interface + * ``validator(meta_validate=False)`` + + +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. @@ -0,0 +1,19 @@ +Copyright (c) 2013 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a951c8a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include *.rst +include COPYING +include tox.ini +recursive-include json * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..627a330 --- /dev/null +++ b/README.rst @@ -0,0 +1,106 @@ +========== +jsonschema +========== + +``jsonschema`` is an implementation of `JSON Schema <http://json-schema.org>`_ +for Python (supporting 2.6+ including Python 3). + +.. code-block:: python + + >>> 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 +-------- + +* Full support for + `Draft 3 <https://python-jsonschema.readthedocs.org/en/latest/validate.html#jsonschema.Draft3Validator>`_ + **and** `Draft 4 <https://python-jsonschema.readthedocs.org/en/latest/validate.html#jsonschema.Draft4Validator>`_ + of the schema. + +* `Lazy validation <https://python-jsonschema.readthedocs.org/en/latest/validate.html#jsonschema.IValidator.iter_errors>`_ + that can iteratively report *all* validation errors. + +* Small and extensible + +* `Programmatic querying <https://python-jsonschema.readthedocs.org/en/latest/errors.html#module-jsonschema>`_ + of which properties or items failed validation. + + +Release Notes +------------- + +``v2.0.0`` adds a better interface for creating and extending validators in the +form of ``jsonschema.validators.create`` and ``jsonschema.validators.extend``. +The documentation is still a bit lacking in this area but it's getting there. +See the tests in ``jsonschema.tests.test_validators`` and the source code if +you'd like to try it out now. ``ValidatorMixin`` has been removed. + +Practically speaking, this affects validators that subclassed a built-in +validator and extended a validator function (presumably with an upcall via +``super``), as the correct way to do so is now to call +``TheValidator.VALIDATORS["extended_validator_fn"]`` directly in a new +validator function (and of course to use ``create``). Examples hopefully coming +soon if more clarification is needed. Patches welcome of course. + +It also fixes a number of issues with ref resolution, one for array indices +(#95) and one for improper handling of unknown URI schemes (#102). + + +Running the Test Suite +---------------------- + +``jsonschema`` uses the wonderful `Tox <http://tox.readthedocs.org>`_ 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. + +Of course you're also free to just run the tests on a single version with your +favorite test runner. The tests live in the ``jsonschema.tests`` package. + + +Community +--------- + +There's a `mailing list <https://groups.google.com/forum/#!forum/jsonschema>`_ for this implementation on Google Groups. + +Please join, and feel free to send questions there. + + +Contributing +------------ + +I'm Julian Berman. + +``jsonschema`` is on `GitHub <http://github.com/Julian/jsonschema>`_. + +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/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..b9772eb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jsonschema.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jsonschema.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/jsonschema" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jsonschema" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..77c6436 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its containing dir. + +from textwrap import dedent +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +ext_paths = [os.path.abspath(os.path.pardir), os.path.dirname(__file__)] +sys.path = ext_paths + sys.path + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'jsonschema_role', +] + +cache_path = "_cache" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'jsonschema' +copyright = u'2013, Julian Berman' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# version: The short X.Y version +# release: The full version, including alpha/beta/rc tags. +from jsonschema import __version__ as release +version = release.partition("-")[0] + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build', "_cache", "_static", "_templates"] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +doctest_global_setup = dedent(""" + from __future__ import print_function + from jsonschema import * +""") + +intersphinx_mapping = {"python": ("http://docs.python.org/3.2", None)} + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'pyramid' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'jsonschemadoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'jsonschema.tex', u'jsonschema Documentation', + u'Julian Berman', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'jsonschema', u'jsonschema Documentation', + [u'Julian Berman'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'jsonschema', u'jsonschema Documentation', + u'Julian Berman', 'jsonschema', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/docs/creating.rst b/docs/creating.rst new file mode 100644 index 0000000..6c42162 --- /dev/null +++ b/docs/creating.rst @@ -0,0 +1,91 @@ +.. _creating-validators: + +================================ +Creating or Extending Validators +================================ + +.. currentmodule:: jsonschema.validators + +.. autofunction:: create + + Create a new validator. + + :argument dict meta_schema: the meta schema for the new validator + + :argument dict validators: a mapping from validator names to functions that + validate the given name. Each function should take 4 arguments: a + validator instance, the value of the current validator property in the + instance being validated, the instance, and the schema. + + :argument str version: an identifier for the version that this validator + will validate. If provided, the returned validator class will have its + ``__name__`` set to include the version, and also will have + :func:`validates` automatically called for the given version. + + :argument dict default_types: a default mapping to use for instances of the + validator when mapping between JSON types to Python types. The default + for this argument is probably fine. Instances of the returned validator + can still have their types customized on a per-instance basis. + + :returns: an :class:`jsonschema.IValidator` + + +.. autofunction:: extend + + Create a new validator that extends an existing validator. + + :argument jsonschema.IValidator validator: an existing validator + + :argument dict validators: a set of new validators to add to the new + validator. + + .. note:: + + Any validators with the same name as an existing one will + (silently) replace the old validator entirely. + + If you wish to extend an old validator, call it directly in the + replacing validator function by retrieving it using + ``OldValidator.VALIDATORS["the validator"]``. + + :argument str version: a version for the new validator + + :returns: an :class:`jsonschema.IValidator` + + .. note:: Meta Schemas + + The new validator will just keep the old validator's meta schema. + + If you wish to change or extend the meta schema in the new validator, + modify ``META_SCHEMA`` directly on the returned class. + + The meta schema on the new validator will not be a copy, so you'll + probably want to copy it before modifying it to not affect the old + validator. + + +.. autofunction:: validator_for + + Retrieve the validator appropriate for validating the given schema. + + Uses the :validator:`$schema` property that should be present in the given + schema to look up the appropriate validator. + + :argument schema: the schema to look at + :argument default: the default to return if the appropriate validator + cannot be determined. If unprovided, the default will be to just return + :class:`Draft4Validator` + + +.. autofunction:: validates + + +Creating Validation Errors +-------------------------- + +Any validating function that validates against a subschema should call +:meth:`ValidatorMixin.descend`, rather than :meth:`ValidatorMixin.iter_errors`. +If it recurses into the instance, or schema, it should pass one or both of the +``path`` or ``schema_path`` arguments to :meth:`ValidatorMixin.descend` in +order to properly maintain where in the instance or schema respsectively the +error occurred. diff --git a/docs/doc-requirements.txt b/docs/doc-requirements.txt new file mode 100644 index 0000000..ab90481 --- /dev/null +++ b/docs/doc-requirements.txt @@ -0,0 +1 @@ +lxml diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..9f63c25 --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,303 @@ +========================== +Handling Validation Errors +========================== + +.. currentmodule:: jsonschema + +When an invalid instance is encountered, a :exc:`ValidationError` will be +raised or returned, depending on which method or function is used. + +.. autoexception:: ValidationError + + The instance didn't properly validate under the provided schema. + + The information carried by an error roughly breaks down into: + + =============== ================= ======================== + What Happened Why Did It Happen What Was Being Validated + =============== ================= ======================== + :attr:`message` :attr:`context` :attr:`instance` + + :attr:`cause` :attr:`path` + + :attr:`schema` + + :attr:`schema_path` + + :attr:`validator` + + :attr:`validator_value` + =============== ================= ======================== + + + .. attribute:: message + + A human readable message explaining the error. + + .. attribute:: validator + + The failed `validator + <http://json-schema.org/latest/json-schema-validation.html#anchor12>`_. + + .. attribute:: validator_value + + The value for the failed validator in the schema. + + .. attribute:: schema + + The full schema that this error came from. This is potentially a + subschema from within the schema that was passed into the validator, or + even an entirely different schema if a :validator:`$ref` was followed. + + .. attribute:: schema_path + + A :class:`collections.deque` containing the path to the failed + validator within the schema. + + .. attribute:: path + + A :class:`collections.deque` containing the path to the offending + element within the instance. The deque can be empty if the error + happened at the root of the instance. + + .. attribute:: instance + + The instance that was being validated. This will differ from the + instance originally passed into validate if the validator was in the + process of validating a (possibly nested) element within the top-level + instance. The path within the top-level instance (i.e. + :attr:`ValidationError.path`) could be used to find this object, but it + is provided for convenience. + + .. attribute:: context + + If the error was caused by errors in subschemas, the list of errors + from the subschemas will be available on this property. The + :attr:`.schema_path` and :attr:`.path` of these errors will be relative + to the parent error. + + .. attribute:: cause + + If the error was caused by a *non*-validation error, the exception + object will be here. Currently this is only used for the exception + raised by a failed format checker in :meth:`FormatChecker.check`. + + +In case an invalid schema itself is encountered, a :exc:`SchemaError` is +raised. + +.. autoexception:: SchemaError + + The provided schema is malformed. + + The same attributes are present as for :exc:`ValidationError`\s. + + +These attributes can be clarified with a short example: + +.. testcode:: + + schema = { + "items": { + "anyOf": [ + {"type": "string", "maxLength": 2}, + {"type": "integer", "minimum": 5} + ] + } + } + instance = [{}, 3, "foo"] + v = Draft4Validator(schema) + errors = sorted(v.iter_errors(instance), key=lambda e: e.path) + +The error messages in this situation are not very helpful on their own. + +.. testcode:: + + for error in errors: + print(error.message) + +outputs: + +.. testoutput:: + + {} is not valid under any of the given schemas + 3 is not valid under any of the given schemas + 'foo' is not valid under any of the given schemas + +If we look at :attr:`~ValidationError.path` on each of the errors, we can find +out which elements in the instance correspond to each of the errors. In +this example, :attr:`~ValidationError.path` will have only one element, which +will be the index in our list. + +.. testcode:: + + for error in errors: + print(list(error.path)) + +.. testoutput:: + + [0] + [1] + [2] + +Since our schema contained nested subschemas, it can be helpful to look at +the specific part of the instance and subschema that caused each of the errors. +This can be seen with the :attr:`~ValidationError.instance` and +:attr:`~ValidationError.schema` attributes. + +With validators like :validator:`anyOf`, the :attr:`~ValidationError.context` +attribute can be used to see the sub-errors which caused the failure. Since +these errors actually came from two separate subschemas, it can be helpful to +look at the :attr:`~ValidationError.schema_path` attribute as well to see where +exactly in the schema each of these errors come from. In the case of sub-errors +from the :attr:`~ValidationError.context` attribute, this path will be relative +to the :attr:`~ValidationError.schema_path` of the parent error. + +.. testcode:: + + for error in errors: + for suberror in sorted(error.context, key=lambda e: e.schema_path): + print(list(suberror.schema_path), suberror.message, sep=", ") + +.. testoutput:: + + [0, 'type'], {} is not of type 'string' + [1, 'type'], {} is not of type 'integer' + [0, 'type'], 3 is not of type 'string' + [1, 'minimum'], 3.0 is less than the minimum of 5 + [0, 'maxLength'], 'foo' is too long + [1, 'type'], 'foo' is not of type 'integer' + +The string representation of an error combines some of these attributes for +easier debugging. + +.. testcode:: + + print(errors[1]) + +.. testoutput:: + + 3 is not valid under any of the given schemas + + Failed validating 'anyOf' in schema['items']: + {'anyOf': [{'maxLength': 2, 'type': 'string'}, + {'minimum': 5, 'type': 'integer'}]} + + On instance[1]: + 3 + + +ErrorTrees +---------- + +If you want to programmatically be able to query which properties or validators +failed when validating a given instance, you probably will want to do so using +:class:`ErrorTree` objects. + +.. autoclass:: ErrorTree + :members: + :special-members: + :exclude-members: __dict__,__weakref__ + + .. attribute:: errors + + The mapping of validator names to the error objects (usually + :class:`ValidationError`\s) at this level of the tree. + +Consider the following example: + +.. testcode:: + + schema = { + "type" : "array", + "items" : {"type" : "number", "enum" : [1, 2, 3]}, + "minItems" : 3, + } + instance = ["spam", 2] + +For clarity's sake, the given instance has three errors under this schema: + +.. testcode:: + + v = Draft3Validator(schema) + for error in sorted(v.iter_errors(["spam", 2]), key=str): + print(error.message) + +.. testoutput:: + + 'spam' is not of type 'number' + 'spam' is not one of [1, 2, 3] + ['spam', 2] is too short + +Let's construct an :class:`ErrorTree` so that we can query the errors a bit +more easily than by just iterating over the error objects. + +.. testcode:: + + tree = ErrorTree(v.iter_errors(instance)) + +As you can see, :class:`ErrorTree` takes an iterable of +:class:`ValidationError`\s when constructing a tree so you can directly pass it +the return value of a validator's :attr:`~IValidator.iter_errors` method. + +:class:`ErrorTree`\s support a number of useful operations. The first one we +might want to perform is to check whether a given element in our instance +failed validation. We do so using the :keyword:`in` operator: + +.. doctest:: + + >>> 0 in tree + True + + >>> 1 in tree + False + +The interpretation here is that the 0th index into the instance (``"spam"``) +did have an error (in fact it had 2), while the 1th index (``2``) did not (i.e. +it was valid). + +If we want to see which errors a child had, we index into the tree and look at +the :attr:`~ErrorTree.errors` attribute. + +.. doctest:: + + >>> sorted(tree[0].errors) + ['enum', 'type'] + +Here we see that the :validator:`enum` and :validator:`type` validators failed +for index ``0``. In fact :attr:`~ErrorTree.errors` is a dict, whose values are +the :class:`ValidationError`\s, so we can get at those directly if we want +them. + +.. doctest:: + + >>> print(tree[0].errors["type"].message) + 'spam' is not of type 'number' + +Of course this means that if we want to know if a given validator failed for a +given index, we check for its presence in :attr:`~ErrorTree.errors`: + +.. doctest:: + + >>> "enum" in tree[0].errors + True + + >>> "minimum" in tree[0].errors + False + +Finally, if you were paying close enough attention, you'll notice that we +haven't seen our :validator:`minItems` error appear anywhere yet. This is +because :validator:`minItems` is an error that applies globally to the instance +itself. So it appears in the root node of the tree. + +.. doctest:: + + >>> "minItems" in tree.errors + True + +That's all you need to know to use error trees. + +To summarize, each tree contains child trees that can be accessed by indexing +the tree to get the corresponding child tree for a given index into the +instance. Each tree and child has a :attr:`~ErrorTree.errors` attribute, a +dict, that maps the failed validator to the corresponding validation error. diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..32775ac --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,94 @@ +========================== +Frequently Asked Questions +========================== + + +Why doesn't my schema that has a default property actually set the default on my instance? +------------------------------------------------------------------------------------------ + +The basic answer is that the specification does not require that +:validator:`default` actually do anything. + +For an inkling as to *why* it doesn't actually do anything, consider that none +of the other validators modify the instance either. More importantly, having +:validator:`default` modify the instance can produce quite peculiar things. +It's perfectly valid (and perhaps even useful) to have a default that is not +valid under the schema it lives in! So an instance modified by the default +would pass validation the first time, but fail the second! + +Still, filling in defaults is a thing that is useful. :mod:`jsonschema` allows +you to :doc:`define your own validators <creating>`, so you can easily create a +:class:`IValidator` that does do default setting. Here's some code to get you +started: + + .. code-block:: python + + from jsonschema import Draft4Validator, validators + + + def extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + for property, subschema in properties.iteritems(): + if "default" in subschema: + instance.setdefault(property, subschema["default"]) + + return validators.extend( + validator_class, {"properties" : set_defaults}, + ) + + + DefaultValidatingDraft4Validator = extend_with_default(Draft4Validator) + + +See the above-linked document for more info on how this works, but basically, +it just extends the :validator:`properties` validator on a +:class:`Draft4Validator` to then go ahed and update all the defaults. + +If you're interested in a more interesting solution to a larger class of these +types of transformations, keep an eye on `Seep +<https://github.com/Julian/Seep>`_, which is an experimental data +transformation and extraction library written on top of :mod:`jsonschema`. + + +How do jsonschema version numbers work? +--------------------------------------- + +``jsonschema`` tries to follow the `Semantic Versioning <http://semver.org/>`_ +specification. + +This means broadly that no backwards-incompatible changes should be made in +minor releases (and certainly not in dot releases). + +The full picture requires defining what constitutes a backwards-incompatible +change. + +The following are simple examples of things considered public API, and +therefore should *not* be changed without bumping a major version number: + + * module names and contents, when not marked private by Python convention + (a single leading underscore) + + * function and object signature (parameter order and name) + +The following are *not* considered public API and may change without notice: + + * the exact wording and contents of error messages; typical + reasons to do this seem to involve unit tests. API users are + encouraged to use the extensive introspection provided in + :class:`~jsonschema.exceptions.ValidationError`\s instead to make + meaningful assertions about what failed. + + * the order in which validation errors are returned or raised + + * anything marked private + +With the exception of the last of those, flippant changes are avoided, but +changes can and will be made if there is improvement to be had. Feel free to +open an issue ticket if there is a specific issue or question worth raising. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..96a09e2 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,52 @@ +========== +jsonschema +========== + + +.. module:: jsonschema + + +``jsonschema`` is an implementation of `JSON Schema <http://json-schema.org>`_ +for Python (supporting 2.6+ including Python 3). + +.. code-block:: python + + >>> 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' + + +Contents: + +.. toctree:: + :maxdepth: 2 + + validate + errors + references + creating + faq + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/jsonschema_role.py b/docs/jsonschema_role.py new file mode 100644 index 0000000..1209145 --- /dev/null +++ b/docs/jsonschema_role.py @@ -0,0 +1,123 @@ +from datetime import datetime +from docutils import nodes +import errno +import os + +try: + import urllib2 as urllib +except ImportError: + import urllib.request as urllib + +from lxml import html + + +VALIDATION_SPEC = "http://json-schema.org/latest/json-schema-validation.html" + + +def setup(app): + """ + Install the plugin. + + :argument sphinx.application.Sphinx app: the Sphinx application context + + """ + + app.add_config_value("cache_path", "_cache", "") + + try: + os.makedirs(app.config.cache_path) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + path = os.path.join(app.config.cache_path, "spec.html") + spec = fetch_or_load(path) + app.add_role("validator", docutils_sucks(spec)) + + +def fetch_or_load(spec_path): + """ + Fetch a new specification or use the cache if it's current. + + :argument cache_path: the path to a cached specification + + """ + + headers = {} + + try: + modified = datetime.utcfromtimestamp(os.path.getmtime(spec_path)) + date = modified.strftime("%a, %d %b %Y %I:%M:%S UTC") + headers["If-Modified-Since"] = date + except OSError as error: + if error.errno != errno.ENOENT: + raise + + request = urllib.Request(VALIDATION_SPEC, headers=headers) + response = urllib.urlopen(request) + + if response.code == 200: + with open(spec_path, "w+b") as spec: + spec.writelines(response) + spec.seek(0) + return html.parse(spec) + + with open(spec_path) as spec: + return html.parse(spec) + + +def docutils_sucks(spec): + """ + Yeah. + + It doesn't allow using a class because it does stupid stuff like try to set + attributes on the callable object rather than just keeping a dict. + + """ + + base_url = VALIDATION_SPEC + ref_url = "http://json-schema.org/latest/json-schema-core.html#anchor25" + schema_url = "http://json-schema.org/latest/json-schema-core.html#anchor22" + + def validator(name, raw_text, text, lineno, inliner): + """ + Link to the JSON Schema documentation for a validator. + + :argument str name: the name of the role in the document + :argument str raw_source: the raw text (role with argument) + :argument str text: the argument given to the role + :argument int lineno: the line number + :argument docutils.parsers.rst.states.Inliner inliner: the inliner + + :returns: 2-tuple of nodes to insert into the document and an iterable + of system messages, both possibly empty + + """ + + if text == "$ref": + return [nodes.reference(raw_text, text, refuri=ref_url)], [] + elif text == "$schema": + return [nodes.reference(raw_text, text, refuri=schema_url)], [] + + xpath = "//h3[re:match(text(), '(^|\W)\"?{0}\"?($|\W,)', 'i')]" + header = spec.xpath( + xpath.format(text), + namespaces={"re": "http://exslt.org/regular-expressions"}, + ) + + if len(header) == 0: + inliner.reporter.warning( + "Didn't find a target for {0}".format(text), + ) + uri = base_url + else: + if len(header) > 1: + inliner.reporter.info( + "Found multiple targets for {0}".format(text), + ) + uri = base_url + "#" + header[0].getprevious().attrib["name"] + + reference = nodes.reference(raw_text, text, refuri=uri) + return [reference], [] + + return validator diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fcb914f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^<target^>` where ^<target^> is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\jsonschema.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\jsonschema.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/references.rst b/docs/references.rst new file mode 100644 index 0000000..9f24299 --- /dev/null +++ b/docs/references.rst @@ -0,0 +1,13 @@ +========================= +Resolving JSON References +========================= + + +.. currentmodule:: jsonschema + +.. autoclass:: RefResolver + :members: + +.. autoexception:: RefResolutionError + + A JSON reference failed to resolve. diff --git a/docs/validate.rst b/docs/validate.rst new file mode 100644 index 0000000..5500950 --- /dev/null +++ b/docs/validate.rst @@ -0,0 +1,264 @@ +================= +Schema Validation +================= + + +.. currentmodule:: jsonschema + + +The Basics +---------- + +The simplest way to validate an instance under a given schema is to use the +:func:`validate` function. + +.. autofunction:: validate + +The Validator Interface +----------------------- + +:mod:`jsonschema` defines an (informal) interface that all validators should +adhere to. + +.. class:: IValidator(schema, types=(), resolver=None, format_checker=None) + + :argument dict schema: the schema that the validator will validate with. It + is assumed to be valid, and providing an invalid + schema can lead to undefined behavior. See + :meth:`IValidator.check_schema` to validate a schema + first. + :argument types: Override or extend the list of known types when validating + the :validator:`type` property. Should map strings (type + names) to class objects that will be checked via + :func:`isinstance`. See :ref:`validating-types` for + details. + :type types: dict or iterable of 2-tuples + :argument resolver: an instance of :class:`RefResolver` that will be used + to resolve :validator:`$ref` properties (JSON + references). If unprovided, one will be created. + :argument format_checker: an instance of :class:`FormatChecker` whose + :meth:`~conforms` method will be called to check + and see if instances conform to each + :validator:`format` property present in the + schema. If unprovided, no validation will be done + for :validator:`format`. + + .. attribute:: DEFAULT_TYPES + + The default mapping of JSON types to Python types used when validating + :validator:`type` properties in JSON schemas. + + .. attribute:: META_SCHEMA + + An object representing the validator's meta schema (the schema that + describes valid schemas in the given version). + + .. attribute:: VALIDATORS + + A mapping of validators (:class:`str`\s) to functions that validate the + validator property with that name. For more information see + :ref:`creating-validators`. + + .. attribute:: schema + + The schema that was passed in when initializing the validator. + + + .. classmethod:: check_schema(schema) + + Validate the given schema against the validator's :attr:`META_SCHEMA`. + + :raises: :exc:`SchemaError` if the schema is invalid + + .. method:: is_type(instance, type) + + Check if the instance is of the given (JSON Schema) type. + + :type type: str + :rtype: bool + :raises: :exc:`UnknownType` if ``type`` is not a known type. + + The special type ``"any"`` is valid for any given instance. + + .. method:: is_valid(instance) + + Check if the instance is valid under the current :attr:`schema`. + + :rtype: bool + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).is_valid([2, 3, 4]) + False + + .. method:: iter_errors(instance) + + Lazily yield each of the validation errors in the given instance. + + :rtype: an iterable of :exc:`ValidationError`\s + + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Draft3Validator(schema) + >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): + ... print(error.message) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + + .. method:: validate(instance) + + Check if the instance is valid under the current :attr:`schema`. + + :raises: :exc:`ValidationError` if the instance is invalid + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).validate([2, 3, 4]) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + +All of the :ref:`versioned validators <versioned-validators>` that are included +with :mod:`jsonschema` adhere to the interface, and implementors of validators +that extend or complement the ones included should adhere to it as well. For +more information see :ref:`creating-validators`. + + +.. _validating-types: + +Validating With Additional Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Occasionally it can be useful to provide additional or alternate types when +validating the JSON Schema's :validator:`type` property. Validators allow this +by taking a ``types`` argument on construction that specifies additional types, +or which can be used to specify a different set of Python types to map to a +given JSON type. + +:mod:`jsonschema` tries to strike a balance between performance in the common +case and generality. For instance, JSON Schema defines a ``number`` type, which +can be validated with a schema such as ``{"type" : "number"}``. By default, +this will accept instances of Python :class:`numbers.Number`. This includes in +particular :class:`int`\s and :class:`float`\s, along with +:class:`decimal.Decimal` objects, :class:`complex` numbers etc. For +``integer`` and ``object``, however, rather than checking for +:class:`numbers.Integral` and :class:`collections.abc.Mapping`, +:mod:`jsonschema` simply checks for :class:`int` and :class:`dict`, since the +more general instance checks can introduce significant slowdown, especially +given how common validating these types are. + +If you *do* want the generality, or just want to add a few specific additional +types as being acceptible for a validator, :class:`IValidator`\s have a +``types`` argument that can be used to provide additional or new types. + +.. code-block:: python + + class MyInteger(object): + ... + + Draft3Validator( + schema={"type" : "number"}, + types={"number" : (numbers.Number, MyInteger)}, + ) + +The list of default Python types for each JSON type is available on each +validator in the :attr:`IValidator.DEFAULT_TYPES` attribute. Note that you +need to specify all types to match if you override one of the existing JSON +types, so you may want to access the set of default types when specifying your +additional type. + +.. _versioned-validators: + +Versioned Validators +-------------------- + +:mod:`jsonschema` ships with validators for various versions of the JSON Schema +specification. For details on the methods and attributes that each validator +provides see the :class:`IValidator` interface, which each validator +implements. + +.. autoclass:: Draft3Validator + +.. autoclass:: Draft4Validator + + +Validating Formats +------------------ + +JSON Schema defines the :validator:`format` property which can be used to check +if primitive types (``string``\s, ``number``\s, ``boolean``\s) conform to +well-defined formats. By default, no validation is enforced, but optionally, +validation can be enabled by hooking in a format-checking object into an +:class:`IValidator`. + +.. doctest:: + + >>> validate("localhost", {"format" : "hostname"}) + >>> validate( + ... "-12", {"format" : "hostname"}, format_checker=FormatChecker(), + ... ) + Traceback (most recent call last): + ... + ValidationError: "-12" is not a "hostname" + +.. autoclass:: FormatChecker + :members: + :exclude-members: cls_checks + + .. attribute:: checkers + + A mapping of currently known formats to tuple of functions that + validate them and errors that should be caught. New checkers can be + added and removed either per-instance or globally for all checkers + using the :meth:`FormatChecker.checks` or + :meth:`FormatChecker.cls_checks` decorators respectively. + + .. classmethod:: cls_checks(format, raises=()) + + Register a decorated function as *globally* validating a new format. + + Any instance created after this function is called will pick up the + supplied checker. + + :argument str format: the format that the decorated function will check + :argument Exception raises: the exception(s) raised by the decorated + function when an invalid instance is found. The exception object + will be accessible as the :attr:`ValidationError.cause` attribute + of the resulting validation error. + + + +There are a number of default checkers that :class:`FormatChecker`\s know how +to validate. Their names can be viewed by inspecting the +:attr:`FormatChecker.checkers` attribute. Certain checkers will only be +available if an appropriate package is available for use. The available +checkers, along with their requirement (if any,) are listed below. + +========== ==================== +Checker Notes +========== ==================== +hostname +ipv4 +ipv6 OS must have :func:`socket.inet_pton` function +email +uri requires rfc3987_ +date-time requires strict-rfc3339_ [#]_ +date +time +regex +color requires webcolors_ +========== ==================== + + +.. [#] For backwards compatibility, isodate_ is also supported, but it will + allow any `ISO 8601 <http://en.wikipedia.org/wiki/ISO_8601>`_ date-time, + not just `RFC 3339 <http://www.ietf.org/rfc/rfc3339.txt>`_ as mandated by + the JSON Schema specification. + + +.. _isodate: http://pypi.python.org/pypi/isodate/ +.. _rfc3987: http://pypi.python.org/pypi/rfc3987/ +.. _strict-rfc3339: http://pypi.python.org/pypi/strict-rfc3339/ +.. _webcolors: http://pypi.python.org/pypi/webcolors/ diff --git a/json/.gitignore b/json/.gitignore new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/json/.gitignore @@ -0,0 +1 @@ +TODO diff --git a/json/.travis.yml b/json/.travis.yml new file mode 100644 index 0000000..deecd61 --- /dev/null +++ b/json/.travis.yml @@ -0,0 +1,4 @@ +language: python +python: "2.7" +install: pip install jsonschema +script: bin/jsonschema_suite check diff --git a/README.md b/json/README.md index 4320685..4320685 100644 --- a/README.md +++ b/json/README.md diff --git a/bin/jsonschema_suite b/json/bin/jsonschema_suite index 96108c8..96108c8 100755 --- a/bin/jsonschema_suite +++ b/json/bin/jsonschema_suite diff --git a/remotes/folder/folderInteger.json b/json/remotes/folder/folderInteger.json index dbe5c75..dbe5c75 100644 --- a/remotes/folder/folderInteger.json +++ b/json/remotes/folder/folderInteger.json diff --git a/remotes/integer.json b/json/remotes/integer.json index dbe5c75..dbe5c75 100644 --- a/remotes/integer.json +++ b/json/remotes/integer.json diff --git a/remotes/subSchemas.json b/json/remotes/subSchemas.json index 8b6d8f8..8b6d8f8 100644 --- a/remotes/subSchemas.json +++ b/json/remotes/subSchemas.json diff --git a/tests/draft3/additionalItems.json b/json/tests/draft3/additionalItems.json index 6d4bff5..6d4bff5 100644 --- a/tests/draft3/additionalItems.json +++ b/json/tests/draft3/additionalItems.json diff --git a/tests/draft3/additionalProperties.json b/json/tests/draft3/additionalProperties.json index eb334c9..eb334c9 100644 --- a/tests/draft3/additionalProperties.json +++ b/json/tests/draft3/additionalProperties.json diff --git a/tests/draft3/dependencies.json b/json/tests/draft3/dependencies.json index 2f6ae48..2f6ae48 100644 --- a/tests/draft3/dependencies.json +++ b/json/tests/draft3/dependencies.json diff --git a/tests/draft3/disallow.json b/json/tests/draft3/disallow.json index a5c9d90..a5c9d90 100644 --- a/tests/draft3/disallow.json +++ b/json/tests/draft3/disallow.json diff --git a/tests/draft3/divisibleBy.json b/json/tests/draft3/divisibleBy.json index ef7cc14..ef7cc14 100644 --- a/tests/draft3/divisibleBy.json +++ b/json/tests/draft3/divisibleBy.json diff --git a/tests/draft3/enum.json b/json/tests/draft3/enum.json index a539edb..a539edb 100644 --- a/tests/draft3/enum.json +++ b/json/tests/draft3/enum.json diff --git a/tests/draft3/extends.json b/json/tests/draft3/extends.json index 909bce5..909bce5 100644 --- a/tests/draft3/extends.json +++ b/json/tests/draft3/extends.json diff --git a/tests/draft3/items.json b/json/tests/draft3/items.json index f5e18a1..f5e18a1 100644 --- a/tests/draft3/items.json +++ b/json/tests/draft3/items.json diff --git a/tests/draft3/maxItems.json b/json/tests/draft3/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft3/maxItems.json +++ b/json/tests/draft3/maxItems.json diff --git a/tests/draft3/maxLength.json b/json/tests/draft3/maxLength.json index 561767b..561767b 100644 --- a/tests/draft3/maxLength.json +++ b/json/tests/draft3/maxLength.json diff --git a/tests/draft3/maximum.json b/json/tests/draft3/maximum.json index 86c7b89..86c7b89 100644 --- a/tests/draft3/maximum.json +++ b/json/tests/draft3/maximum.json diff --git a/tests/draft3/minItems.json b/json/tests/draft3/minItems.json index ed51188..ed51188 100644 --- a/tests/draft3/minItems.json +++ b/json/tests/draft3/minItems.json diff --git a/tests/draft3/minLength.json b/json/tests/draft3/minLength.json index e9c14b1..e9c14b1 100644 --- a/tests/draft3/minLength.json +++ b/json/tests/draft3/minLength.json diff --git a/tests/draft3/minimum.json b/json/tests/draft3/minimum.json index d5bf000..d5bf000 100644 --- a/tests/draft3/minimum.json +++ b/json/tests/draft3/minimum.json diff --git a/tests/draft3/optional/bignum.json b/json/tests/draft3/optional/bignum.json index 7b4755c..7b4755c 100644 --- a/tests/draft3/optional/bignum.json +++ b/json/tests/draft3/optional/bignum.json diff --git a/tests/draft3/optional/format.json b/json/tests/draft3/optional/format.json index 1ff461a..1ff461a 100644 --- a/tests/draft3/optional/format.json +++ b/json/tests/draft3/optional/format.json diff --git a/tests/draft3/optional/jsregex.json b/json/tests/draft3/optional/jsregex.json index 03fe977..03fe977 100644 --- a/tests/draft3/optional/jsregex.json +++ b/json/tests/draft3/optional/jsregex.json diff --git a/tests/draft3/optional/zeroTerminatedFloats.json b/json/tests/draft3/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft3/optional/zeroTerminatedFloats.json +++ b/json/tests/draft3/optional/zeroTerminatedFloats.json diff --git a/tests/draft3/pattern.json b/json/tests/draft3/pattern.json index befc4b5..befc4b5 100644 --- a/tests/draft3/pattern.json +++ b/json/tests/draft3/pattern.json diff --git a/tests/draft3/patternProperties.json b/json/tests/draft3/patternProperties.json index 18586e5..18586e5 100644 --- a/tests/draft3/patternProperties.json +++ b/json/tests/draft3/patternProperties.json diff --git a/tests/draft3/properties.json b/json/tests/draft3/properties.json index cd1644d..cd1644d 100644 --- a/tests/draft3/properties.json +++ b/json/tests/draft3/properties.json diff --git a/tests/draft3/ref.json b/json/tests/draft3/ref.json index c984019..c984019 100644 --- a/tests/draft3/ref.json +++ b/json/tests/draft3/ref.json diff --git a/tests/draft3/refRemote.json b/json/tests/draft3/refRemote.json index 4ca8047..4ca8047 100644 --- a/tests/draft3/refRemote.json +++ b/json/tests/draft3/refRemote.json diff --git a/tests/draft3/required.json b/json/tests/draft3/required.json index aaaf024..aaaf024 100644 --- a/tests/draft3/required.json +++ b/json/tests/draft3/required.json diff --git a/tests/draft3/type.json b/json/tests/draft3/type.json index 8f10889..8f10889 100644 --- a/tests/draft3/type.json +++ b/json/tests/draft3/type.json diff --git a/tests/draft3/uniqueItems.json b/json/tests/draft3/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft3/uniqueItems.json +++ b/json/tests/draft3/uniqueItems.json diff --git a/tests/draft4/additionalItems.json b/json/tests/draft4/additionalItems.json index 521745c..521745c 100644 --- a/tests/draft4/additionalItems.json +++ b/json/tests/draft4/additionalItems.json diff --git a/tests/draft4/additionalProperties.json b/json/tests/draft4/additionalProperties.json index eb334c9..eb334c9 100644 --- a/tests/draft4/additionalProperties.json +++ b/json/tests/draft4/additionalProperties.json diff --git a/tests/draft4/allOf.json b/json/tests/draft4/allOf.json index bbb5f89..bbb5f89 100644 --- a/tests/draft4/allOf.json +++ b/json/tests/draft4/allOf.json diff --git a/tests/draft4/anyOf.json b/json/tests/draft4/anyOf.json index a58714a..a58714a 100644 --- a/tests/draft4/anyOf.json +++ b/json/tests/draft4/anyOf.json diff --git a/tests/draft4/definitions.json b/json/tests/draft4/definitions.json index cf935a3..cf935a3 100644 --- a/tests/draft4/definitions.json +++ b/json/tests/draft4/definitions.json diff --git a/tests/draft4/dependencies.json b/json/tests/draft4/dependencies.json index 7b9b16a..7b9b16a 100644 --- a/tests/draft4/dependencies.json +++ b/json/tests/draft4/dependencies.json diff --git a/tests/draft4/enum.json b/json/tests/draft4/enum.json index a539edb..a539edb 100644 --- a/tests/draft4/enum.json +++ b/json/tests/draft4/enum.json diff --git a/tests/draft4/items.json b/json/tests/draft4/items.json index f5e18a1..f5e18a1 100644 --- a/tests/draft4/items.json +++ b/json/tests/draft4/items.json diff --git a/tests/draft4/maxItems.json b/json/tests/draft4/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft4/maxItems.json +++ b/json/tests/draft4/maxItems.json diff --git a/tests/draft4/maxLength.json b/json/tests/draft4/maxLength.json index 561767b..561767b 100644 --- a/tests/draft4/maxLength.json +++ b/json/tests/draft4/maxLength.json diff --git a/tests/draft4/maxProperties.json b/json/tests/draft4/maxProperties.json index d282446..d282446 100644 --- a/tests/draft4/maxProperties.json +++ b/json/tests/draft4/maxProperties.json diff --git a/tests/draft4/maximum.json b/json/tests/draft4/maximum.json index 86c7b89..86c7b89 100644 --- a/tests/draft4/maximum.json +++ b/json/tests/draft4/maximum.json diff --git a/tests/draft4/minItems.json b/json/tests/draft4/minItems.json index ed51188..ed51188 100644 --- a/tests/draft4/minItems.json +++ b/json/tests/draft4/minItems.json diff --git a/tests/draft4/minLength.json b/json/tests/draft4/minLength.json index e9c14b1..e9c14b1 100644 --- a/tests/draft4/minLength.json +++ b/json/tests/draft4/minLength.json diff --git a/tests/draft4/minProperties.json b/json/tests/draft4/minProperties.json index a72c7d2..a72c7d2 100644 --- a/tests/draft4/minProperties.json +++ b/json/tests/draft4/minProperties.json diff --git a/tests/draft4/minimum.json b/json/tests/draft4/minimum.json index d5bf000..d5bf000 100644 --- a/tests/draft4/minimum.json +++ b/json/tests/draft4/minimum.json diff --git a/tests/draft4/multipleOf.json b/json/tests/draft4/multipleOf.json index ca3b761..ca3b761 100644 --- a/tests/draft4/multipleOf.json +++ b/json/tests/draft4/multipleOf.json diff --git a/tests/draft4/not.json b/json/tests/draft4/not.json index 2cdc979..2cdc979 100644 --- a/tests/draft4/not.json +++ b/json/tests/draft4/not.json diff --git a/tests/draft4/oneOf.json b/json/tests/draft4/oneOf.json index 1eaa4e4..1eaa4e4 100644 --- a/tests/draft4/oneOf.json +++ b/json/tests/draft4/oneOf.json diff --git a/tests/draft4/optional/bignum.json b/json/tests/draft4/optional/bignum.json index 7b4755c..7b4755c 100644 --- a/tests/draft4/optional/bignum.json +++ b/json/tests/draft4/optional/bignum.json diff --git a/tests/draft4/optional/format.json b/json/tests/draft4/optional/format.json index cba8fc3..cba8fc3 100644 --- a/tests/draft4/optional/format.json +++ b/json/tests/draft4/optional/format.json diff --git a/tests/draft4/optional/zeroTerminatedFloats.json b/json/tests/draft4/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft4/optional/zeroTerminatedFloats.json +++ b/json/tests/draft4/optional/zeroTerminatedFloats.json diff --git a/tests/draft4/pattern.json b/json/tests/draft4/pattern.json index befc4b5..befc4b5 100644 --- a/tests/draft4/pattern.json +++ b/json/tests/draft4/pattern.json diff --git a/tests/draft4/patternProperties.json b/json/tests/draft4/patternProperties.json index 18586e5..18586e5 100644 --- a/tests/draft4/patternProperties.json +++ b/json/tests/draft4/patternProperties.json diff --git a/tests/draft4/properties.json b/json/tests/draft4/properties.json index cd1644d..cd1644d 100644 --- a/tests/draft4/properties.json +++ b/json/tests/draft4/properties.json diff --git a/tests/draft4/ref.json b/json/tests/draft4/ref.json index b38ff03..b38ff03 100644 --- a/tests/draft4/ref.json +++ b/json/tests/draft4/ref.json diff --git a/tests/draft4/refRemote.json b/json/tests/draft4/refRemote.json index 4ca8047..4ca8047 100644 --- a/tests/draft4/refRemote.json +++ b/json/tests/draft4/refRemote.json diff --git a/tests/draft4/required.json b/json/tests/draft4/required.json index 612f73f..612f73f 100644 --- a/tests/draft4/required.json +++ b/json/tests/draft4/required.json diff --git a/tests/draft4/type.json b/json/tests/draft4/type.json index 257f051..257f051 100644 --- a/tests/draft4/type.json +++ b/json/tests/draft4/type.json diff --git a/tests/draft4/uniqueItems.json b/json/tests/draft4/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft4/uniqueItems.json +++ b/json/tests/draft4/uniqueItems.json diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py new file mode 100644 index 0000000..f0a5e99 --- /dev/null +++ b/jsonschema/__init__.py @@ -0,0 +1,26 @@ +""" +An implementation of JSON Schema for Python + +The main functionality is provided by the validator classes for each of the +supported JSON Schema versions. + +Most commonly, :func:`validate` is the quickest way to simply validate a given +instance under a schema, and will create a validator for you. + +""" + +from jsonschema.exceptions import ( + FormatError, RefResolutionError, SchemaError, ValidationError +) +from jsonschema._format import ( + FormatChecker, draft3_format_checker, draft4_format_checker, +) +from jsonschema.validators import ( + ErrorTree, Draft3Validator, Draft4Validator, RefResolver, validate +) + + +__version__ = "2.1.0-dev" + + +# flake8: noqa diff --git a/jsonschema/_format.py b/jsonschema/_format.py new file mode 100644 index 0000000..edfe97d --- /dev/null +++ b/jsonschema/_format.py @@ -0,0 +1,206 @@ +import datetime +import re +import socket + +from jsonschema.exceptions import FormatError + + +class FormatChecker(object): + """ + A ``format`` property checker. + + JSON Schema does not mandate that the ``format`` property actually do any + validation. If validation is desired however, instances of this class can + be hooked into validators to enable format validation. + + :class:`FormatChecker` objects always return ``True`` when asked about + formats that they do not know how to validate. + + To check a custom format using a function that takes an instance and + returns a ``bool``, use the :meth:`FormatChecker.checks` or + :meth:`FormatChecker.cls_checks` decorators. + + :argument iterable formats: the known formats to validate. This argument + can be used to limit which formats will be used + during validation. + + """ + + checkers = {} + + def __init__(self, formats=None): + if formats is None: + self.checkers = self.checkers.copy() + else: + self.checkers = dict((k, self.checkers[k]) for k in formats) + + def checks(self, format, raises=()): + """ + Register a decorated function as validating a new format. + + :argument str format: the format that the decorated function will check + :argument Exception raises: the exception(s) raised by the decorated + function when an invalid instance is found. The exception object + will be accessible as the :attr:`ValidationError.cause` attribute + of the resulting validation error. + + """ + + def _checks(func): + self.checkers[format] = (func, raises) + return func + return _checks + + cls_checks = classmethod(checks) + + def check(self, instance, format): + """ + Check whether the instance conforms to the given format. + + :argument instance: the instance to check + :type: any primitive type (str, number, bool) + :argument str format: the format that instance should conform to + :raises: :exc:`FormatError` if instance does not conform to format + + """ + + if format not in self.checkers: + return + + func, raises = self.checkers[format] + result, cause = None, None + try: + result = func(instance) + except raises as e: + cause = e + if not result: + raise FormatError( + "%r is not a %r" % (instance, format), cause=cause, + ) + + def conforms(self, instance, format): + """ + Check whether the instance conforms to the given format. + + :argument instance: the instance to check + :type: any primitive type (str, number, bool) + :argument str format: the format that instance should conform to + :rtype: bool + + """ + + try: + self.check(instance, format) + except FormatError: + return False + else: + return True + + +_draft_checkers = {"draft3": [], "draft4": []} + + +def _checks_drafts(both=None, draft3=None, draft4=None, raises=()): + draft3 = draft3 or both + draft4 = draft4 or both + + def wrap(func): + if draft3: + _draft_checkers["draft3"].append(draft3) + func = FormatChecker.cls_checks(draft3, raises)(func) + if draft4: + _draft_checkers["draft4"].append(draft4) + func = FormatChecker.cls_checks(draft4, raises)(func) + return func + return wrap + + +@_checks_drafts("email") +def is_email(instance): + return "@" in instance + + +_checks_drafts(draft3="ip-address", draft4="ipv4", raises=socket.error)( + socket.inet_aton +) + + +if hasattr(socket, "inet_pton"): + @_checks_drafts("ipv6", raises=socket.error) + def is_ipv6(instance): + return socket.inet_pton(socket.AF_INET6, instance) + + +@_checks_drafts(draft3="host-name", draft4="hostname") +def is_host_name(instance): + pattern = "^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$" + if not re.match(pattern, instance): + return False + components = instance.split(".") + for component in components: + if len(component) > 63: + return False + return True + + +try: + import rfc3987 +except ImportError: + pass +else: + @_checks_drafts("uri", raises=ValueError) + def is_uri(instance): + return rfc3987.parse(instance, rule="URI_reference") + + +try: + import strict_rfc3339 +except ImportError: + try: + import isodate + except ImportError: + pass + else: + _err = (ValueError, isodate.ISO8601Error) + _checks_drafts("date-time", raises=_err)(isodate.parse_datetime) +else: + _checks_drafts("date-time")(strict_rfc3339.validate_rfc3339) + + +_checks_drafts("regex", raises=re.error)(re.compile) + + +@_checks_drafts(draft3="date", raises=ValueError) +def is_date(instance): + return datetime.datetime.strptime(instance, "%Y-%m-%d") + + +@_checks_drafts(draft3="time", raises=ValueError) +def is_time(instance): + return datetime.datetime.strptime(instance, "%H:%M:%S") + + +try: + import webcolors +except ImportError: + pass +else: + def is_css_color_code(instance): + return webcolors.normalize_hex(instance) + + + @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) + def is_css21_color(instance): + if instance.lower() in webcolors.css21_names_to_hex: + return True + return is_css_color_code(instance) + + + def is_css3_color(instance): + if instance.lower() in webcolors.css3_names_to_hex: + return True + return is_css_color_code(instance) + + +draft3_format_checker = FormatChecker(_draft_checkers["draft3"]) +draft4_format_checker = FormatChecker(_draft_checkers["draft4"]) diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py new file mode 100644 index 0000000..44a577a --- /dev/null +++ b/jsonschema/_utils.py @@ -0,0 +1,217 @@ +import itertools +import json +import re +import os + +from jsonschema.compat import str_types, MutableMapping, urlsplit + + +class URIDict(MutableMapping): + """ + Dictionary which uses normalized URIs as keys. + + """ + + def normalize(self, uri): + return urlsplit(uri).geturl() + + def __init__(self, *args, **kwargs): + self.store = dict() + self.store.update(*args, **kwargs) + + def __getitem__(self, uri): + return self.store[self.normalize(uri)] + + def __setitem__(self, uri, value): + self.store[self.normalize(uri)] = value + + def __delitem__(self, uri): + del self.store[self.normalize(uri)] + + def __iter__(self): + return iter(self.store) + + def __len__(self): + return len(self.store) + + def __repr__(self): + return repr(self.store) + + +class Unset(object): + """ + An as-of-yet unset attribute or unprovided default parameter. + + """ + + def __repr__(self): + return "<unset>" + + +def load_schema(name): + """ + Load a schema from ./schemas/``name``.json and return it. + + """ + schemadir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'schemas' + ) + schemapath = os.path.join(schemadir, '%s.json' % (name,)) + with open(schemapath) as f: + return json.load(f) + + +def indent(string, times=1): + """ + A dumb version of :func:`textwrap.indent` from Python 3.3. + + """ + + return "\n".join(" " * (4 * times) + line for line in string.splitlines()) + + +def format_as_index(indices): + """ + Construct a single string containing indexing operations for the indices. + + For example, [1, 2, "foo"] -> [1][2]["foo"] + + :type indices: sequence + + """ + + if not indices: + return "" + return "[%s]" % "][".join(repr(index) for index in indices) + + +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. + + """ + + if len(extras) == 1: + verb = "was" + else: + verb = "were" + 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 ensure_list(thing): + """ + Wrap ``thing`` in a list if it's a single str. + + Otherwise, return it unchanged. + + """ + + if isinstance(thing, str_types): + return [thing] + return thing + + +def unbool(element, true=object(), false=object()): + """ + A hack to make True and 1 and False and 0 unique for ``uniq``. + + """ + + if element is True: + return true + elif element is False: + return false + return element + + +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(unbool(i) for i in container)) == len(container) + except TypeError: + try: + sort = sorted(unbool(i) for i in container) + sliced = itertools.islice(sort, 1, None) + for i, j in zip(sort, sliced): + if i == j: + return False + except (NotImplementedError, TypeError): + seen = [] + for e in container: + e = unbool(e) + if e in seen: + return False + seen.append(e) + return True diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py new file mode 100644 index 0000000..bfcd1c1 --- /dev/null +++ b/jsonschema/_validators.py @@ -0,0 +1,363 @@ +import re + +from jsonschema import _utils +from jsonschema.exceptions import FormatError, ValidationError +from jsonschema.compat import iteritems + + +FLOAT_TOLERANCE = 10 ** -15 + + +def patternProperties(validator, patternProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for pattern, subschema in iteritems(patternProperties): + for k, v in iteritems(instance): + if re.search(pattern, k): + for error in validator.descend( + v, subschema, path=k, schema_path=pattern + ): + yield error + + +def additionalProperties(validator, aP, instance, schema): + if not validator.is_type(instance, "object"): + return + + extras = set(_utils.find_additional_properties(instance, schema)) + + if validator.is_type(aP, "object"): + for extra in extras: + for error in validator.descend(instance[extra], aP, path=extra): + yield error + elif not aP and extras: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % _utils.extras_msg(extras)) + + +def items(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "object"): + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + else: + for (index, item), subschema in zip(enumerate(instance), items): + for error in validator.descend( + item, subschema, path=index, schema_path=index + ): + yield error + + +def additionalItems(validator, aI, instance, schema): + if ( + not validator.is_type(instance, "array") or + validator.is_type(schema.get("items", {}), "object") + ): + return + + len_items = len(schema.get("items", [])) + if validator.is_type(aI, "object"): + for index, item in enumerate(instance[len_items:], start=len_items): + for error in validator.descend(item, aI, path=index): + yield error + elif not aI and len(instance) > len(schema.get("items", [])): + error = "Additional items are not allowed (%s %s unexpected)" + yield ValidationError( + error % + _utils.extras_msg(instance[len(schema.get("items", [])):]) + ) + + +def minimum(validator, minimum, instance, schema): + if not validator.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 maximum(validator, maximum, instance, schema): + if not validator.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 multipleOf(validator, dB, instance, schema): + if not validator.is_type(instance, "number"): + return + + if isinstance(dB, float): + mod = instance % dB + failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE + else: + failed = instance % dB + + if failed: + yield ValidationError("%r is not a multiple of %r" % (instance, dB)) + + +def minItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) < mI: + yield ValidationError("%r is too short" % (instance,)) + + +def maxItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) > mI: + yield ValidationError("%r is too long" % (instance,)) + + +def uniqueItems(validator, uI, instance, schema): + if ( + uI and + validator.is_type(instance, "array") and + not _utils.uniq(instance) + ): + yield ValidationError("%r has non-unique elements" % instance) + + +def pattern(validator, patrn, instance, schema): + if ( + validator.is_type(instance, "string") and + not re.search(patrn, instance) + ): + yield ValidationError("%r does not match %r" % (instance, patrn)) + + +def format(validator, format, instance, schema): + if ( + validator.format_checker is not None and + validator.is_type(instance, "string") + ): + try: + validator.format_checker.check(instance, format) + except FormatError as error: + yield ValidationError(error.message, cause=error.cause) + + +def minLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) < mL: + yield ValidationError("%r is too short" % (instance,)) + + +def maxLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) > mL: + yield ValidationError("%r is too long" % (instance,)) + + +def dependencies(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in iteritems(dependencies): + if property not in instance: + continue + + if validator.is_type(dependency, "object"): + for error in validator.descend( + instance, dependency, schema_path=property + ): + yield error + else: + dependencies = _utils.ensure_list(dependency) + for dependency in dependencies: + if dependency not in instance: + yield ValidationError( + "%r is a dependency of %r" % (dependency, property) + ) + + +def enum(validator, enums, instance, schema): + if instance not in enums: + yield ValidationError("%r is not one of %r" % (instance, enums)) + + +def ref(validator, ref, instance, schema): + with validator.resolver.resolving(ref) as resolved: + for error in validator.descend(instance, resolved): + yield error + + +def type_draft3(validator, types, instance, schema): + types = _utils.ensure_list(types) + + all_errors = [] + for index, type in enumerate(types): + if type == "any": + return + if validator.is_type(type, "object"): + errors = list(validator.descend(instance, type, schema_path=index)) + if not errors: + return + all_errors.extend(errors) + elif validator.is_type(type, "string"): + if validator.is_type(instance, type): + return + else: + yield ValidationError( + _utils.types_msg(instance, types), context=all_errors, + ) + + +def properties_draft3(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ): + yield error + elif subschema.get("required", False): + error = ValidationError("%r is a required property" % property) + error._set( + validator="required", + validator_value=subschema["required"], + instance=instance, + schema=schema, + ) + error.path.appendleft(property) + error.schema_path.extend([property, "required"]) + yield error + + +def disallow_draft3(validator, disallow, instance, schema): + for disallowed in _utils.ensure_list(disallow): + if validator.is_valid(instance, {"type" : [disallowed]}): + yield ValidationError( + "%r is disallowed for %r" % (disallowed, instance) + ) + + +def extends_draft3(validator, extends, instance, schema): + if validator.is_type(extends, "object"): + for error in validator.descend(instance, extends): + yield error + return + for index, subschema in enumerate(extends): + for error in validator.descend(instance, subschema, schema_path=index): + yield error + + +def type_draft4(validator, types, instance, schema): + types = _utils.ensure_list(types) + + if not any(validator.is_type(instance, type) for type in types): + yield ValidationError(_utils.types_msg(instance, types)) + + +def properties_draft4(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ): + yield error + + +def required_draft4(validator, required, instance, schema): + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + yield ValidationError("%r is a required property" % property) + + +def minProperties_draft4(validator, mP, instance, schema): + if validator.is_type(instance, "object") and len(instance) < mP: + yield ValidationError( + "%r does not have enough properties" % (instance,) + ) + + +def maxProperties_draft4(validator, mP, instance, schema): + if not validator.is_type(instance, "object"): + return + if validator.is_type(instance, "object") and len(instance) > mP: + yield ValidationError("%r has too many properties" % (instance,)) + + +def allOf_draft4(validator, allOf, instance, schema): + for index, subschema in enumerate(allOf): + for error in validator.descend(instance, subschema, schema_path=index): + yield error + + +def oneOf_draft4(validator, oneOf, instance, schema): + subschemas = enumerate(oneOf) + all_errors = [] + for index, subschema in subschemas: + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + first_valid = subschema + break + all_errors.extend(errs) + else: + yield ValidationError( + "%r is not valid under any of the given schemas" % (instance,), + context=all_errors, + ) + + more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)] + if more_valid: + more_valid.append(first_valid) + reprs = ", ".join(repr(schema) for schema in more_valid) + yield ValidationError( + "%r is valid under each of %s" % (instance, reprs) + ) + + +def anyOf_draft4(validator, anyOf, instance, schema): + all_errors = [] + for index, subschema in enumerate(anyOf): + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + break + all_errors.extend(errs) + else: + yield ValidationError( + "%r is not valid under any of the given schemas" % (instance,), + context=all_errors, + ) + + +def not_draft4(validator, not_schema, instance, schema): + if validator.is_valid(instance, not_schema): + yield ValidationError( + "%r is not allowed for %r" % (not_schema, instance) + ) diff --git a/jsonschema/compat.py b/jsonschema/compat.py new file mode 100644 index 0000000..e5394f0 --- /dev/null +++ b/jsonschema/compat.py @@ -0,0 +1,51 @@ +from __future__ import unicode_literals +import sys +import operator + +try: + from collections import MutableMapping, Sequence # noqa +except ImportError: + from collections.abc import MutableMapping, Sequence # noqa + +PY3 = sys.version_info[0] >= 3 + +if PY3: + zip = zip + from urllib.parse import ( + unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit + ) + from urllib.request import urlopen + str_types = str, + int_types = int, + iteritems = operator.methodcaller("items") +else: + from itertools import izip as zip # noqa + from urlparse import ( + urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa + ) + from urllib import unquote # noqa + from urllib2 import urlopen # noqa + str_types = basestring + int_types = int, long + iteritems = operator.methodcaller("iteritems") + + +# On python < 3.3 fragments are not handled properly with unknown schemes +def urlsplit(url): + scheme, netloc, path, query, fragment = _urlsplit(url) + if "#" in path: + path, fragment = path.split("#", 1) + return SplitResult(scheme, netloc, path, query, fragment) + + +def urldefrag(url): + if "#" in url: + s, n, p, q, frag = urlsplit(url) + defrag = urlunsplit((s, n, p, q, '')) + else: + defrag = url + frag = '' + return defrag, frag + + +# flake8: noqa diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py new file mode 100644 index 0000000..f1d3e42 --- /dev/null +++ b/jsonschema/exceptions.py @@ -0,0 +1,112 @@ +import collections +import pprint +import textwrap + +from jsonschema import _utils +from jsonschema.compat import PY3, iteritems + + +_unset = _utils.Unset() + + +class _Error(Exception): + def __init__( + self, message, validator=_unset, path=(), cause=None, context=(), + validator_value=_unset, instance=_unset, schema=_unset, schema_path=(), + ): + self.message = message + self.path = collections.deque(path) + self.schema_path = collections.deque(schema_path) + self.context = list(context) + self.cause = self.__cause__ = cause + self.validator = validator + self.validator_value = validator_value + self.instance = instance + self.schema = schema + + @classmethod + def create_from(cls, other): + return cls( + message=other.message, + cause=other.cause, + context=other.context, + path=other.path, + schema_path=other.schema_path, + validator=other.validator, + validator_value=other.validator_value, + instance=other.instance, + schema=other.schema, + ) + + def _set(self, **kwargs): + for k, v in iteritems(kwargs): + if getattr(self, k) is _unset: + setattr(self, k, v) + + def __repr__(self): + return "<%s: %r>" % (self.__class__.__name__, self.message) + + def __str__(self): + return unicode(self).encode("utf-8") + + def __unicode__(self): + if _unset in ( + self.validator, self.validator_value, self.instance, self.schema, + ): + return self.message + + path = _utils.format_as_index(self.path) + schema_path = _utils.format_as_index(list(self.schema_path)[:-1]) + + pschema = pprint.pformat(self.schema, width=72) + pinstance = pprint.pformat(self.instance, width=72) + return self.message + textwrap.dedent(""" + + Failed validating %r in schema%s: + %s + + On instance%s: + %s + """.rstrip() + ) % ( + self.validator, + schema_path, + _utils.indent(pschema), + path, + _utils.indent(pinstance), + ) + + if PY3: + __str__ = __unicode__ + + +class ValidationError(_Error): + pass + + +class SchemaError(_Error): + pass + + +class RefResolutionError(Exception): + pass + + +class UnknownType(Exception): + pass + + +class FormatError(Exception): + def __init__(self, message, cause=None): + super(FormatError, self).__init__(message, cause) + self.message = message + self.cause = self.__cause__ = cause + + def __str__(self): + return self.message.encode("utf-8") + + def __unicode__(self): + return self.message + + if PY3: + __str__ = __unicode__ diff --git a/jsonschema/schemas/draft3.json b/jsonschema/schemas/draft3.json new file mode 100644 index 0000000..5bcefe3 --- /dev/null +++ b/jsonschema/schemas/draft3.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "dependencies": { + "exclusiveMaximum": "maximum", + "exclusiveMinimum": "minimum" + }, + "id": "http://json-schema.org/draft-03/schema#", + "properties": { + "$ref": { + "format": "uri", + "type": "string" + }, + "$schema": { + "format": "uri", + "type": "string" + }, + "additionalItems": { + "default": {}, + "type": [ + { + "$ref": "#" + }, + "boolean" + ] + }, + "additionalProperties": { + "default": {}, + "type": [ + { + "$ref": "#" + }, + "boolean" + ] + }, + "default": { + "type": "any" + }, + "dependencies": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": [ + "string", + "array", + { + "$ref": "#" + } + ] + }, + "default": {}, + "type": [ + "string", + "array", + "object" + ] + }, + "description": { + "type": "string" + }, + "disallow": { + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "type": [ + "string", + "array" + ], + "uniqueItems": true + }, + "divisibleBy": { + "default": 1, + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "enum": { + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "exclusiveMaximum": { + "default": false, + "type": "boolean" + }, + "exclusiveMinimum": { + "default": false, + "type": "boolean" + }, + "extends": { + "default": {}, + "items": { + "$ref": "#" + }, + "type": [ + { + "$ref": "#" + }, + "array" + ] + }, + "format": { + "type": "string" + }, + "id": { + "format": "uri", + "type": "string" + }, + "items": { + "default": {}, + "items": { + "$ref": "#" + }, + "type": [ + { + "$ref": "#" + }, + "array" + ] + }, + "maxDecimal": { + "minimum": 0, + "type": "number" + }, + "maxItems": { + "minimum": 0, + "type": "integer" + }, + "maxLength": { + "type": "integer" + }, + "maximum": { + "type": "number" + }, + "minItems": { + "default": 0, + "minimum": 0, + "type": "integer" + }, + "minLength": { + "default": 0, + "minimum": 0, + "type": "integer" + }, + "minimum": { + "type": "number" + }, + "pattern": { + "format": "regex", + "type": "string" + }, + "patternProperties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "properties": { + "additionalProperties": { + "$ref": "#", + "type": "object" + }, + "default": {}, + "type": "object" + }, + "required": { + "default": false, + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "default": "any", + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "type": [ + "string", + "array" + ], + "uniqueItems": true + }, + "uniqueItems": { + "default": false, + "type": "boolean" + } + }, + "type": "object" +} diff --git a/jsonschema/schemas/draft4.json b/jsonschema/schemas/draft4.json new file mode 100644 index 0000000..fead5ce --- /dev/null +++ b/jsonschema/schemas/draft4.json @@ -0,0 +1,221 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "default": {}, + "definitions": { + "positiveInteger": { + "minimum": 0, + "type": "integer" + }, + "positiveIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/positiveInteger" + }, + { + "default": 0 + } + ] + }, + "schemaArray": { + "items": { + "$ref": "#" + }, + "minItems": 1, + "type": "array" + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + }, + "dependencies": { + "exclusiveMaximum": [ + "maximum" + ], + "exclusiveMinimum": [ + "minimum" + ] + }, + "description": "Core schema meta-schema", + "id": "http://json-schema.org/draft-04/schema#", + "properties": { + "$schema": { + "format": "uri", + "type": "string" + }, + "additionalItems": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "default": {}, + "definitions": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "dependencies": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + }, + "type": "object" + }, + "description": { + "type": "string" + }, + "enum": { + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "exclusiveMaximum": { + "default": false, + "type": "boolean" + }, + "exclusiveMinimum": { + "default": false, + "type": "boolean" + }, + "id": { + "format": "uri", + "type": "string" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": {} + }, + "maxItems": { + "$ref": "#/definitions/positiveInteger" + }, + "maxLength": { + "$ref": "#/definitions/positiveInteger" + }, + "maxProperties": { + "$ref": "#/definitions/positiveInteger" + }, + "maximum": { + "type": "number" + }, + "minItems": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minLength": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minProperties": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minimum": { + "type": "number" + }, + "multipleOf": { + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "not": { + "$ref": "#" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "pattern": { + "format": "regex", + "type": "string" + }, + "patternProperties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "properties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "title": { + "type": "string" + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + }, + "uniqueItems": { + "default": false, + "type": "boolean" + } + }, + "type": "object" +} diff --git a/jsonschema/tests/__init__.py b/jsonschema/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/jsonschema/tests/__init__.py diff --git a/jsonschema/tests/compat.py b/jsonschema/tests/compat.py new file mode 100644 index 0000000..b37483f --- /dev/null +++ b/jsonschema/tests/compat.py @@ -0,0 +1,15 @@ +import sys + + +if sys.version_info[:2] < (2, 7): # pragma: no cover + import unittest2 as unittest +else: + import unittest + +try: + from unittest import mock +except ImportError: + import mock + + +# flake8: noqa diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py new file mode 100644 index 0000000..8392ca1 --- /dev/null +++ b/jsonschema/tests/test_format.py @@ -0,0 +1,63 @@ +""" +Tests for the parts of jsonschema related to the :validator:`format` property. + +""" + +from jsonschema.tests.compat import mock, unittest + +from jsonschema import FormatError, ValidationError, FormatChecker +from jsonschema.validators import Draft4Validator + + +class TestFormatChecker(unittest.TestCase): + def setUp(self): + self.fn = mock.Mock() + + def test_it_can_validate_no_formats(self): + checker = FormatChecker(formats=()) + self.assertFalse(checker.checkers) + + def test_it_raises_a_key_error_for_unknown_formats(self): + with self.assertRaises(KeyError): + FormatChecker(formats=["o noes"]) + + def test_it_can_register_cls_checkers(self): + with mock.patch.dict(FormatChecker.checkers, clear=True): + FormatChecker.cls_checks("new")(self.fn) + self.assertEqual(FormatChecker.checkers, {"new" : (self.fn, ())}) + + def test_it_can_register_checkers(self): + checker = FormatChecker() + checker.checks("new")(self.fn) + self.assertEqual( + checker.checkers, + dict(FormatChecker.checkers, new=(self.fn, ())) + ) + + def test_it_catches_registered_errors(self): + checker = FormatChecker() + cause = self.fn.side_effect = ValueError() + + checker.checks("foo", raises=ValueError)(self.fn) + + with self.assertRaises(FormatError) as cm: + checker.check("bar", "foo") + + self.assertIs(cm.exception.cause, cause) + self.assertIs(cm.exception.__cause__, cause) + + # Unregistered errors should not be caught + self.fn.side_effect = AttributeError + with self.assertRaises(AttributeError): + checker.check("bar", "foo") + + def test_format_error_causes_become_validation_error_causes(self): + checker = FormatChecker() + checker.checks("foo", raises=ValueError)(self.fn) + cause = self.fn.side_effect = ValueError() + validator = Draft4Validator({"format" : "foo"}, format_checker=checker) + + with self.assertRaises(ValidationError) as cm: + validator.validate("bar") + + self.assertIs(cm.exception.__cause__, cause) diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py new file mode 100644 index 0000000..e95dbbe --- /dev/null +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -0,0 +1,249 @@ +""" +Test runner for the JSON Schema official test suite + +Tests comprehensive correctness of each draft's validator. + +See https://github.com/json-schema/JSON-Schema-Test-Suite for details. + +""" + +from decimal import Decimal +import glob +import json +import io +import itertools +import os +import re +import subprocess + +try: + from sys import pypy_version_info +except ImportError: + pypy_version_info = None + +from jsonschema import ( + FormatError, SchemaError, ValidationError, Draft3Validator, + Draft4Validator, FormatChecker, draft3_format_checker, + draft4_format_checker, validate, +) +from jsonschema.compat import PY3 +from jsonschema.tests.compat import mock, unittest +import jsonschema + + +REPO_ROOT = os.path.join(os.path.dirname(jsonschema.__file__), os.path.pardir) +SUITE = os.getenv("JSON_SCHEMA_TEST_SUITE", os.path.join(REPO_ROOT, "json")) + +if not os.path.isdir(SUITE): + raise ValueError( + "Can't find the JSON-Schema-Test-Suite directory. Set the " + "'JSON_SCHEMA_TEST_SUITE' environment variable or run the tests from " + "alongside a checkout of the suite." + ) + +TESTS_DIR = os.path.join(SUITE, "tests") +JSONSCHEMA_SUITE = os.path.join(SUITE, "bin", "jsonschema_suite") + +REMOTES = subprocess.Popen( + ["python", JSONSCHEMA_SUITE, "remotes"], stdout=subprocess.PIPE, +).stdout +if PY3: + REMOTES = io.TextIOWrapper(REMOTES) +REMOTES = json.load(REMOTES) + + +def make_case(schema, data, valid, name): + if valid: + def test_case(self): + kwargs = getattr(self, "validator_kwargs", {}) + validate(data, schema, cls=self.validator_class, **kwargs) + else: + def test_case(self): + kwargs = getattr(self, "validator_kwargs", {}) + with self.assertRaises(ValidationError): + validate(data, schema, cls=self.validator_class, **kwargs) + + if not PY3: + name = name.encode("utf-8") + test_case.__name__ = name + + return test_case + + +def maybe_skip(skip, test, case): + if skip is not None: + reason = skip(case) + if reason is not None: + test = unittest.skip(reason)(test) + return test + + +def load_json_cases(tests_glob, ignore_glob="", basedir=TESTS_DIR, skip=None): + if ignore_glob: + ignore_glob = os.path.join(basedir, ignore_glob) + + def add_test_methods(test_class): + ignored = set(glob.iglob(ignore_glob)) + + for filename in glob.iglob(os.path.join(basedir, tests_glob)): + if filename in ignored: + continue + + validating, _ = os.path.splitext(os.path.basename(filename)) + id = itertools.count(1) + + with open(filename) as test_file: + for case in json.load(test_file): + for test in case["tests"]: + name = "test_%s_%s_%s" % ( + validating, + next(id), + re.sub(r"[\W ]+", "_", test["description"]), + ) + assert not hasattr(test_class, name), name + + test_case = make_case( + data=test["data"], + schema=case["schema"], + valid=test["valid"], + name=name, + ) + test_case = maybe_skip(skip, test_case, case) + setattr(test_class, name, test_case) + + return test_class + return add_test_methods + + +class TypesMixin(object): + @unittest.skipIf(PY3, "In Python 3 json.load always produces unicode") + def test_string_a_bytestring_is_a_string(self): + self.validator_class({"type" : "string"}).validate(b"foo") + + +class DecimalMixin(object): + def test_it_can_validate_with_decimals(self): + schema = {"type" : "number"} + validator = self.validator_class( + schema, types={"number" : (int, float, Decimal)} + ) + + for valid in [1, 1.1, Decimal(1) / Decimal(8)]: + validator.validate(valid) + + for invalid in ["foo", {}, [], True, None]: + with self.assertRaises(ValidationError): + validator.validate(invalid) + + +def missing_format(checker): + def missing_format(case): + format = case["schema"].get("format") + if format not in checker.checkers or ( + # datetime.datetime is overzealous about typechecking in <=1.9 + format == "date-time" and + pypy_version_info is not None and + pypy_version_info[:2] <= (1, 9) + ): + return "Format checker {0!r} not found.".format(format) + return missing_format + + +class FormatMixin(object): + def test_it_returns_true_for_formats_it_does_not_know_about(self): + validator = self.validator_class( + {"format" : "carrot"}, format_checker=FormatChecker(), + ) + validator.validate("bugs") + + def test_it_does_not_validate_formats_by_default(self): + validator = self.validator_class({}) + self.assertIsNone(validator.format_checker) + + def test_it_validates_formats_if_a_checker_is_provided(self): + checker = mock.Mock(spec=FormatChecker) + validator = self.validator_class( + {"format" : "foo"}, format_checker=checker, + ) + + validator.validate("bar") + + checker.check.assert_called_once_with("bar", "foo") + + cause = ValueError() + checker.check.side_effect = FormatError('aoeu', cause=cause) + + with self.assertRaises(ValidationError) as cm: + validator.validate("bar") + # Make sure original cause is attached + self.assertIs(cm.exception.cause, cause) + + +@load_json_cases("draft3/*.json", ignore_glob="draft3/refRemote.json") +@load_json_cases( + "draft3/optional/format.json", skip=missing_format(draft3_format_checker) +) +@load_json_cases("draft3/optional/bignum.json") +@load_json_cases("draft3/optional/zeroTerminatedFloats.json") +class TestDraft3(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): + validator_class = Draft3Validator + validator_kwargs = {"format_checker" : draft3_format_checker} + + def test_any_type_is_valid_for_type_any(self): + validator = self.validator_class({"type" : "any"}) + validator.validate(mock.Mock()) + + # TODO: we're in need of more meta schema tests + def test_invalid_properties(self): + with self.assertRaises(SchemaError): + validate({}, {"properties": {"test": True}}, + cls=self.validator_class) + + def test_minItems_invalid_string(self): + with self.assertRaises(SchemaError): + # needs to be an integer + validate([1], {"minItems" : "1"}, cls=self.validator_class) + + +@load_json_cases("draft4/*.json", ignore_glob="draft4/refRemote.json") +@load_json_cases( + "draft4/optional/format.json", skip=missing_format(draft4_format_checker) +) +@load_json_cases("draft4/optional/bignum.json") +@load_json_cases("draft4/optional/zeroTerminatedFloats.json") +class TestDraft4(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): + validator_class = Draft4Validator + validator_kwargs = {"format_checker" : draft4_format_checker} + + # TODO: we're in need of more meta schema tests + def test_invalid_properties(self): + with self.assertRaises(SchemaError): + validate({}, {"properties": {"test": True}}, + cls=self.validator_class) + + def test_minItems_invalid_string(self): + with self.assertRaises(SchemaError): + # needs to be an integer + validate([1], {"minItems" : "1"}, cls=self.validator_class) + + +class RemoteRefResolutionMixin(object): + def setUp(self): + patch = mock.patch("jsonschema.validators.requests") + requests = patch.start() + requests.get.side_effect = self.resolve + self.addCleanup(patch.stop) + + def resolve(self, reference): + _, _, reference = reference.partition("http://localhost:1234/") + return mock.Mock(**{"json.return_value" : REMOTES.get(reference)}) + + +@load_json_cases("draft3/refRemote.json") +class Draft3RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): + validator_class = Draft3Validator + + +@load_json_cases("draft4/refRemote.json") +class Draft4RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): + validator_class = Draft4Validator diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py new file mode 100644 index 0000000..357e388 --- /dev/null +++ b/jsonschema/tests/test_validators.py @@ -0,0 +1,847 @@ +from __future__ import unicode_literals +import contextlib +import json +import pprint +import textwrap + +from jsonschema import FormatChecker, ValidationError +from jsonschema.compat import PY3 +from jsonschema.tests.compat import mock, unittest +from jsonschema.validators import ( + RefResolutionError, UnknownType, ErrorTree, Draft3Validator, + Draft4Validator, RefResolver, create, extend, validator_for, validate, +) + + +class TestCreateAndExtend(unittest.TestCase): + def setUp(self): + self.meta_schema = {"properties" : {"smelly" : {}}} + self.smelly = mock.MagicMock() + self.validators = {"smelly" : self.smelly} + self.types = {"dict" : dict} + self.Validator = create( + meta_schema=self.meta_schema, + validators=self.validators, + default_types=self.types, + ) + + self.validator_value = 12 + self.schema = {"smelly" : self.validator_value} + self.validator = self.Validator(self.schema) + + def test_attrs(self): + self.assertEqual(self.Validator.VALIDATORS, self.validators) + self.assertEqual(self.Validator.META_SCHEMA, self.meta_schema) + self.assertEqual(self.Validator.DEFAULT_TYPES, self.types) + + def test_init(self): + self.assertEqual(self.validator.schema, self.schema) + + def test_iter_errors(self): + instance = "hello" + + self.smelly.return_value = [] + self.assertEqual(list(self.validator.iter_errors(instance)), []) + + error = mock.Mock() + self.smelly.return_value = [error] + self.assertEqual(list(self.validator.iter_errors(instance)), [error]) + + self.smelly.assert_called_with( + self.validator, self.validator_value, instance, self.schema, + ) + + def test_if_a_version_is_provided_it_is_registered(self): + with mock.patch("jsonschema.validators.validates") as validates: + validates.side_effect = lambda version : lambda cls : cls + Validator = create(meta_schema={"id" : "id"}, version="my version") + validates.assert_called_once_with("my version") + self.assertEqual(Validator.__name__, "MyVersionValidator") + + def test_if_a_version_is_not_provided_it_is_not_registered(self): + with mock.patch("jsonschema.validators.validates") as validates: + create(meta_schema={"id" : "id"}) + self.assertFalse(validates.called) + + def test_extend(self): + validators = dict(self.Validator.VALIDATORS) + new = mock.Mock() + + Extended = extend(self.Validator, validators={"a new one" : new}) + + validators.update([("a new one", new)]) + self.assertEqual(Extended.VALIDATORS, validators) + self.assertNotIn("a new one", self.Validator.VALIDATORS) + + self.assertEqual(Extended.META_SCHEMA, self.Validator.META_SCHEMA) + self.assertEqual(Extended.DEFAULT_TYPES, self.Validator.DEFAULT_TYPES) + + +class TestIterErrors(unittest.TestCase): + def setUp(self): + self.validator = Draft3Validator({}) + + def test_iter_errors(self): + instance = [1, 2] + schema = { + "disallow" : "array", + "enum" : [["a", "b", "c"], ["d", "e", "f"]], + "minItems" : 3 + } + + got = (e.message for e in self.validator.iter_errors(instance, schema)) + expected = [ + "%r is disallowed for [1, 2]" % (schema["disallow"],), + "[1, 2] is too short", + "[1, 2] is not one of %r" % (schema["enum"],), + ] + self.assertEqual(sorted(got), sorted(expected)) + + 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(self.validator.iter_errors(instance, schema)) + self.assertEqual(len(errors), 4) + + +class TestValidationErrorMessages(unittest.TestCase): + def message_for(self, instance, schema, *args, **kwargs): + kwargs.setdefault("cls", Draft3Validator) + with self.assertRaises(ValidationError) as e: + validate(instance, schema, *args, **kwargs) + return e.exception.message + + def test_single_type_failure(self): + message = self.message_for(instance=1, schema={"type" : "string"}) + self.assertEqual(message, "1 is not of type %r" % "string") + + def test_single_type_list_failure(self): + message = self.message_for(instance=1, schema={"type" : ["string"]}) + self.assertEqual(message, "1 is not of type %r" % "string") + + def test_multiple_type_failure(self): + types = ("string", "object") + message = self.message_for(instance=1, schema={"type" : list(types)}) + self.assertEqual(message, "1 is not of type %r, %r" % types) + + def test_object_without_title_type_failure(self): + type = {"type" : [{"minimum" : 3}]} + message = self.message_for(instance=1, schema={"type" : [type]}) + self.assertEqual(message, "1 is not of type %r" % (type,)) + + def test_object_with_name_type_failure(self): + name = "Foo" + schema = {"type" : [{"name" : name, "minimum" : 3}]} + message = self.message_for(instance=1, schema=schema) + self.assertEqual(message, "1 is not of type %r" % (name,)) + + def test_dependencies_failure_has_single_element_not_list(self): + depend, on = "bar", "foo" + schema = {"dependencies" : {depend : on}} + message = self.message_for({"bar" : 2}, schema) + self.assertEqual(message, "%r is a dependency of %r" % (on, depend)) + + def test_additionalItems_single_failure(self): + message = self.message_for( + [2], {"items" : [], "additionalItems" : False}, + ) + self.assertIn("(2 was unexpected)", message) + + def test_additionalItems_multiple_failures(self): + message = self.message_for( + [1, 2, 3], {"items" : [], "additionalItems" : False} + ) + self.assertIn("(1, 2, 3 were unexpected)", message) + + def test_additionalProperties_single_failure(self): + additional = "foo" + schema = {"additionalProperties" : False} + message = self.message_for({additional : 2}, schema) + self.assertIn("(%r was unexpected)" % (additional,), message) + + def test_additionalProperties_multiple_failures(self): + schema = {"additionalProperties" : False} + message = self.message_for(dict.fromkeys(["foo", "bar"]), schema) + + self.assertIn(repr("foo"), message) + self.assertIn(repr("bar"), message) + self.assertIn("were unexpected)", message) + + def test_invalid_format_default_message(self): + checker = FormatChecker(formats=()) + check_fn = mock.Mock(return_value=False) + checker.checks("thing")(check_fn) + + schema = {"format" : "thing"} + message = self.message_for("bla", schema, format_checker=checker) + + self.assertIn(repr("bla"), message) + self.assertIn(repr("thing"), message) + self.assertIn("is not a", message) + + +class TestErrorReprStr(unittest.TestCase): + + message = "hello" + + def setUp(self): + self.error = ValidationError( + message=self.message, + validator="type", + validator_value="string", + instance=5, + schema={"type" : "string"}, + ) + + def assertShows(self, message): + if PY3: + message = message.replace("u'", "'") + message = textwrap.dedent(message).rstrip("\n") + + message_line, _, rest = str(self.error).partition("\n") + self.assertEqual(message_line, self.message) + self.assertEqual(rest, message) + + def test_repr(self): + self.assertEqual( + repr(self.error), + "<ValidationError: %r>" % self.message, + ) + + def test_unset_error(self): + error = ValidationError("message") + self.assertEqual(str(error), "message") + + kwargs = { + "validator": "type", + "validator_value": "string", + "instance": 5, + "schema": {"type": "string"} + } + # Just the message should show if any of the attributes are unset + for attr in kwargs: + k = dict(kwargs) + del k[attr] + error = ValidationError("message", **k) + self.assertEqual(str(error), "message") + + def test_empty_paths(self): + self.error.path = self.error.schema_path = [] + self.assertShows( + """ + Failed validating u'type' in schema: + {u'type': u'string'} + + On instance: + 5 + """ + ) + + def test_one_item_paths(self): + self.error.path = [0] + self.error.schema_path = ["items"] + self.assertShows( + """ + Failed validating u'type' in schema: + {u'type': u'string'} + + On instance[0]: + 5 + """ + ) + + def test_multiple_item_paths(self): + self.error.path = [0, "a"] + self.error.schema_path = ["items", 0, 1] + self.assertShows( + """ + Failed validating u'type' in schema[u'items'][0]: + {u'type': u'string'} + + On instance[0][u'a']: + 5 + """ + ) + + def test_uses_pprint(self): + with mock.patch.object(pprint, "pformat") as pformat: + str(self.error) + self.assertGreater(pformat.call_count, 1) # schema + instance + + +class TestValidationErrorDetails(unittest.TestCase): + # TODO: These really need unit tests for each individual validator, rather + # than just these higher level tests. + def test_anyOf(self): + instance = 5 + schema = { + "anyOf": [ + {"minimum": 20}, + {"type": "string"} + ] + } + + validator = Draft4Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "anyOf") + self.assertEqual(list(e.schema_path), ["anyOf"]) + self.assertEqual(e.validator_value, schema["anyOf"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertEqual(list(e.path), []) + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "minimum") + self.assertEqual(list(e1.schema_path), [0, "minimum"]) + self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["anyOf"][0]) + self.assertEqual(list(e1.path), []) + self.assertEqual(len(e1.context), 0) + + self.assertEqual(e2.validator, "type") + self.assertEqual(list(e2.schema_path), [1, "type"]) + self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) + self.assertEqual(e2.instance, instance) + self.assertEqual(e2.schema, schema["anyOf"][1]) + self.assertEqual(list(e2.path), []) + self.assertEqual(len(e2.context), 0) + + def test_type(self): + instance = {"foo": 1} + schema = { + "type": [ + {"type": "integer"}, + { + "type": "object", + "properties": { + "foo": {"enum": [2]} + } + } + ] + } + + validator = Draft3Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "type") + self.assertEqual(list(e.schema_path), ["type"]) + self.assertEqual(e.validator_value, schema["type"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertEqual(list(e.path), []) + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "type") + self.assertEqual(list(e1.schema_path), [0, "type"]) + self.assertEqual(e1.validator_value, schema["type"][0]["type"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["type"][0]) + self.assertEqual(list(e1.path), []) + self.assertEqual(len(e1.context), 0) + + self.assertEqual(e2.validator, "enum") + self.assertEqual( + list(e2.schema_path), + [1, "properties", "foo", "enum"] + ) + self.assertEqual( + e2.validator_value, + schema["type"][1]["properties"]["foo"]["enum"] + ) + self.assertEqual(e2.instance, instance["foo"]) + self.assertEqual(e2.schema, schema["type"][1]["properties"]["foo"]) + self.assertEqual(list(e2.path), ["foo"]) + self.assertEqual(len(e2.context), 0) + + 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]}, + } + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4 = sorted_errors(errors) + + self.assertEqual(list(e1.path), ["bar"]) + self.assertEqual(list(e2.path), ["baz"]) + self.assertEqual(list(e3.path), ["baz"]) + self.assertEqual(list(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}, + } + } + } + } + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4, e5, e6 = sorted_errors(errors) + + self.assertEqual(list(e1.path), []) + self.assertEqual(list(e2.path), [0]) + self.assertEqual(list(e3.path), [1, "bar"]) + self.assertEqual(list(e4.path), [1, "bar", "bar"]) + self.assertEqual(list(e5.path), [1, "bar", "baz"]) + self.assertEqual(list(e6.path), [1, "foo"]) + + self.assertEqual(list(e1.schema_path), ["type"]) + self.assertEqual(list(e2.schema_path), ["items", "type"]) + self.assertEqual( + list(e3.schema_path), ["items", "properties", "bar", "type"], + ) + self.assertEqual( + list(e4.schema_path), + ["items", "properties", "bar", "properties", "bar", "required"], + ) + self.assertEqual( + list(e5.schema_path), + ["items", "properties", "bar", "properties", "baz", "minItems"] + ) + self.assertEqual( + list(e6.schema_path), ["items", "properties", "foo", "enum"], + ) + + 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") + + def test_additionalProperties(self): + instance = {"bar": "bar", "foo": 2} + schema = { + "additionalProperties" : {"type": "integer", "minimum": 5} + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(list(e1.path), ["bar"]) + self.assertEqual(list(e2.path), ["foo"]) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_patternProperties(self): + instance = {"bar": 1, "foo": 2} + schema = { + "patternProperties" : { + "bar": {"type": "string"}, + "foo": {"minimum": 5} + } + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(list(e1.path), ["bar"]) + self.assertEqual(list(e2.path), ["foo"]) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems(self): + instance = ["foo", 1] + schema = { + "items": [], + "additionalItems" : {"type": "integer", "minimum": 5} + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(list(e1.path), [0]) + self.assertEqual(list(e2.path), [1]) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems_with_items(self): + instance = ["foo", "bar", 1] + schema = { + "items": [{}], + "additionalItems" : {"type": "integer", "minimum": 5} + } + + validator = Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(list(e1.path), [1]) + self.assertEqual(list(e2.path), [2]) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + +class TestErrorTree(unittest.TestCase): + def setUp(self): + self.validator = Draft3Validator({}) + + def test_it_knows_how_many_total_errors_it_contains(self): + errors = [mock.MagicMock() for _ in range(8)] + tree = ErrorTree(errors) + self.assertEqual(tree.total_errors, 8) + + def test_it_contains_an_item_if_the_item_had_an_error(self): + errors = [ValidationError("a message", path=["bar"])] + tree = ErrorTree(errors) + self.assertIn("bar", tree) + + def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): + errors = [ValidationError("a message", path=["bar"])] + tree = ErrorTree(errors) + self.assertNotIn("foo", tree) + + def test_validators_that_failed_appear_in_errors_dict(self): + error = ValidationError("a message", validator="foo") + tree = ErrorTree([error]) + self.assertEqual(tree.errors, {"foo" : error}) + + def test_it_creates_a_child_tree_for_each_nested_path(self): + errors = [ + ValidationError("a bar message", path=["bar"]), + ValidationError("a bar -> 0 message", path=["bar", 0]), + ] + tree = ErrorTree(errors) + self.assertIn(0, tree["bar"]) + self.assertNotIn(1, tree["bar"]) + + def test_children_have_their_errors_dicts_built(self): + e1, e2 = ( + ValidationError("message 1", validator="foo", path=["bar", 0]), + ValidationError("message 2", validator="quux", path=["bar", 0]), + ) + tree = ErrorTree([e1, e2]) + self.assertEqual(tree["bar"][0].errors, {"foo" : e1, "quux" : e2}) + + def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): + error = ValidationError("a message", validator="foo", instance=[]) + tree = ErrorTree([error]) + + with self.assertRaises(IndexError): + tree[0] + + def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): + """ + If a validator is dumb (like :validator:`required` in draft 3) and + refers to a path that isn't in the instance, the tree still properly + returns a subtree for that path. + + """ + + error = ValidationError( + "a message", validator="foo", instance={}, path=["foo"], + ) + tree = ErrorTree([error]) + self.assertIsInstance(tree["foo"], ErrorTree) + + +class ValidatorTestMixin(object): + def setUp(self): + self.instance = mock.Mock() + self.schema = {} + self.resolver = mock.Mock() + self.validator = self.validator_class(self.schema) + + def test_valid_instances_are_valid(self): + errors = iter([]) + + with mock.patch.object( + self.validator, "iter_errors", return_value=errors, + ): + self.assertTrue( + self.validator.is_valid(self.instance, self.schema) + ) + + def test_invalid_instances_are_not_valid(self): + errors = iter([mock.Mock()]) + + with mock.patch.object( + self.validator, "iter_errors", return_value=errors, + ): + self.assertFalse( + self.validator.is_valid(self.instance, self.schema) + ) + + def test_non_existent_properties_are_ignored(self): + instance, my_property, my_value = mock.Mock(), mock.Mock(), mock.Mock() + validate(instance=instance, schema={my_property : my_value}) + + def test_it_creates_a_ref_resolver_if_not_provided(self): + self.assertIsInstance(self.validator.resolver, RefResolver) + + def test_it_delegates_to_a_ref_resolver(self): + resolver = RefResolver("", {}) + schema = {"$ref" : mock.Mock()} + + @contextlib.contextmanager + def resolving(): + yield {"type": "integer"} + + with mock.patch.object(resolver, "resolving") as resolve: + resolve.return_value = resolving() + with self.assertRaises(ValidationError): + self.validator_class(schema, resolver=resolver).validate(None) + + resolve.assert_called_once_with(schema["$ref"]) + + def test_is_type_is_true_for_valid_type(self): + self.assertTrue(self.validator.is_type("foo", "string")) + + def test_is_type_is_false_for_invalid_type(self): + self.assertFalse(self.validator.is_type("foo", "array")) + + def test_is_type_evades_bool_inheriting_from_int(self): + self.assertFalse(self.validator.is_type(True, "integer")) + self.assertFalse(self.validator.is_type(True, "number")) + + def test_is_type_raises_exception_for_unknown_type(self): + with self.assertRaises(UnknownType): + self.validator.is_type("foo", object()) + + +class TestDraft3Validator(ValidatorTestMixin, unittest.TestCase): + validator_class = Draft3Validator + + def test_is_type_is_true_for_any_type(self): + self.assertTrue(self.validator.is_valid(mock.Mock(), {"type": "any"})) + + def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): + self.assertTrue(self.validator.is_type(True, "boolean")) + self.assertTrue(self.validator.is_valid(True, {"type": "any"})) + + +class TestDraft4Validator(ValidatorTestMixin, unittest.TestCase): + validator_class = Draft4Validator + + +class TestValidatorFor(unittest.TestCase): + def test_draft_3(self): + schema = {"$schema" : "http://json-schema.org/draft-03/schema"} + self.assertIs(validator_for(schema), Draft3Validator) + + schema = {"$schema" : "http://json-schema.org/draft-03/schema#"} + self.assertIs(validator_for(schema), Draft3Validator) + + def test_draft_4(self): + schema = {"$schema" : "http://json-schema.org/draft-04/schema"} + self.assertIs(validator_for(schema), Draft4Validator) + + schema = {"$schema" : "http://json-schema.org/draft-04/schema#"} + self.assertIs(validator_for(schema), Draft4Validator) + + def test_custom_validator(self): + Validator = create(meta_schema={"id" : "meta schema id"}, version="12") + schema = {"$schema" : "meta schema id"} + self.assertIs(validator_for(schema), Validator) + + def test_validator_for_jsonschema_default(self): + self.assertIs(validator_for({}), Draft4Validator) + + def test_validator_for_custom_default(self): + self.assertIs(validator_for({}, default=None), None) + + +class TestValidate(unittest.TestCase): + def test_draft3_validator_is_chosen(self): + schema = {"$schema" : "http://json-schema.org/draft-03/schema#"} + with mock.patch.object(Draft3Validator, "check_schema") as chk_schema: + validate({}, schema) + chk_schema.assert_called_once_with(schema) + # Make sure it works without the empty fragment + schema = {"$schema" : "http://json-schema.org/draft-03/schema"} + with mock.patch.object(Draft3Validator, "check_schema") as chk_schema: + validate({}, schema) + chk_schema.assert_called_once_with(schema) + + def test_draft4_validator_is_chosen(self): + schema = {"$schema" : "http://json-schema.org/draft-04/schema#"} + with mock.patch.object(Draft4Validator, "check_schema") as chk_schema: + validate({}, schema) + chk_schema.assert_called_once_with(schema) + + def test_draft4_validator_is_the_default(self): + with mock.patch.object(Draft4Validator, "check_schema") as chk_schema: + validate({}, {}) + chk_schema.assert_called_once_with({}) + + +class TestRefResolver(unittest.TestCase): + + base_uri = "" + stored_uri = "foo://stored" + stored_schema = {"stored" : "schema"} + + def setUp(self): + self.referrer = {} + self.store = {self.stored_uri : self.stored_schema} + self.resolver = RefResolver(self.base_uri, self.referrer, self.store) + + def test_it_does_not_retrieve_schema_urls_from_the_network(self): + ref = Draft3Validator.META_SCHEMA["id"] + with mock.patch.object(self.resolver, "resolve_remote") as remote: + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, Draft3Validator.META_SCHEMA) + self.assertFalse(remote.called) + + def test_it_resolves_local_refs(self): + ref = "#/properties/foo" + self.referrer["properties"] = {"foo" : object()} + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, self.referrer["properties"]["foo"]) + + def test_it_resolves_local_refs_with_id(self): + schema = {"id": "foo://bar/schema#", "a": {"foo": "bar"}} + resolver = RefResolver.from_schema(schema) + with resolver.resolving("#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + with resolver.resolving("foo://bar/schema#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + + def test_it_retrieves_stored_refs(self): + with self.resolver.resolving(self.stored_uri) as resolved: + self.assertIs(resolved, self.stored_schema) + + self.resolver.store["cached_ref"] = {"foo" : 12} + with self.resolver.resolving("cached_ref#/foo") as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_requests(self): + ref = "http://bar#baz" + schema = {"baz" : 12} + + with mock.patch("jsonschema.validators.requests") as requests: + requests.get.return_value.json.return_value = schema + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 12) + requests.get.assert_called_once_with("http://bar") + + def test_it_retrieves_unstored_refs_via_urlopen(self): + ref = "http://bar#baz" + schema = {"baz" : 12} + + with mock.patch("jsonschema.validators.requests", None): + with mock.patch("jsonschema.validators.urlopen") as urlopen: + urlopen.return_value.read.return_value = ( + json.dumps(schema).encode("utf8")) + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 12) + urlopen.assert_called_once_with("http://bar") + + def test_it_can_construct_a_base_uri_from_a_schema(self): + schema = {"id" : "foo"} + resolver = RefResolver.from_schema(schema) + self.assertEqual(resolver.base_uri, "foo") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo#") as resolved: + self.assertEqual(resolved, schema) + + def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): + schema = {} + resolver = RefResolver.from_schema(schema) + self.assertEqual(resolver.base_uri, "") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + + def test_custom_uri_scheme_handlers(self): + schema = {"foo": "bar"} + ref = "foo://bar" + foo_handler = mock.Mock(return_value=schema) + resolver = RefResolver("", {}, handlers={"foo": foo_handler}) + with resolver.resolving(ref) as resolved: + self.assertEqual(resolved, schema) + foo_handler.assert_called_once_with(ref) + + def test_cache_remote_on(self): + ref = "foo://bar" + foo_handler = mock.Mock() + resolver = RefResolver( + "", {}, cache_remote=True, handlers={"foo" : foo_handler}, + ) + with resolver.resolving(ref): + pass + with resolver.resolving(ref): + pass + foo_handler.assert_called_once_with(ref) + + def test_cache_remote_off(self): + ref = "foo://bar" + foo_handler = mock.Mock() + resolver = RefResolver( + "", {}, cache_remote=False, handlers={"foo" : foo_handler}, + ) + with resolver.resolving(ref): + pass + with resolver.resolving(ref): + pass + self.assertEqual(foo_handler.call_count, 2) + + def test_if_you_give_it_junk_you_get_a_resolution_error(self): + ref = "foo://bar" + foo_handler = mock.Mock(side_effect=ValueError("Oh no! What's this?")) + resolver = RefResolver("", {}, handlers={"foo" : foo_handler}) + with self.assertRaises(RefResolutionError) as err: + with resolver.resolving(ref): + pass + self.assertEqual(str(err.exception), "Oh no! What's this?") + + +def sorted_errors(errors): + def key(error): + return ( + [str(e) for e in error.path], + [str(e) for e in error.schema_path] + ) + return sorted(errors, key=key) diff --git a/jsonschema/validators.py b/jsonschema/validators.py new file mode 100644 index 0000000..22abf57 --- /dev/null +++ b/jsonschema/validators.py @@ -0,0 +1,508 @@ +from __future__ import division, unicode_literals + +import collections +import contextlib +import json +import numbers + +try: + import requests +except ImportError: + requests = None + +from jsonschema import _utils, _validators +from jsonschema.compat import ( + PY3, Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, + str_types, int_types, iteritems, +) +from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType + + +_unset = _utils.Unset() + +validators = {} +meta_schemas = _utils.URIDict() + + +def validates(version): + """ + Register the decorated validator for a ``version`` of the specification. + + Registered validators and their meta schemas will be considered when + parsing ``$schema`` properties' URIs. + + :argument str version: an identifier to use as the version's name + :returns: a class decorator to decorate the validator with the version + + """ + + def _validates(cls): + validators[version] = cls + if "id" in cls.META_SCHEMA: + meta_schemas[cls.META_SCHEMA["id"]] = cls + return cls + return _validates + + +def create(meta_schema, validators=(), version=None, default_types=None): # noqa + if default_types is None: + default_types = { + "array" : list, "boolean" : bool, "integer" : int_types, + "null" : type(None), "number" : numbers.Number, "object" : dict, + "string" : str_types, + } + + class Validator(object): + VALIDATORS = dict(validators) + META_SCHEMA = dict(meta_schema) + DEFAULT_TYPES = dict(default_types) + + def __init__( + self, schema, types=(), resolver=None, format_checker=None, + ): + self._types = dict(self.DEFAULT_TYPES) + self._types.update(types) + + if resolver is None: + resolver = RefResolver.from_schema(schema) + + self.resolver = resolver + self.format_checker = format_checker + self.schema = schema + + @classmethod + def check_schema(cls, schema): + for error in cls(cls.META_SCHEMA).iter_errors(schema): + raise SchemaError.create_from(error) + + def iter_errors(self, instance, _schema=None): + if _schema is None: + _schema = self.schema + + with self.resolver.in_scope(_schema.get("id", "")): + ref = _schema.get("$ref") + if ref is not None: + validators = [("$ref", ref)] + else: + validators = iteritems(_schema) + + for k, v in validators: + validator = self.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(self, v, instance, _schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=_schema, + ) + if k != "$ref": + error.schema_path.appendleft(k) + yield error + + def descend(self, instance, schema, path=None, schema_path=None): + for error in self.iter_errors(instance, schema): + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error + + def validate(self, *args, **kwargs): + for error in self.iter_errors(*args, **kwargs): + raise error + + def is_type(self, instance, type): + if type not in self._types: + raise UnknownType(type) + pytypes = self._types[type] + + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + pytypes = _utils.flatten(pytypes) + is_number = any( + issubclass(pytype, numbers.Number) for pytype in pytypes + ) + if is_number and bool not in pytypes: + return False + return isinstance(instance, pytypes) + + def is_valid(self, instance, _schema=None): + error = next(self.iter_errors(instance, _schema), None) + return error is None + + if version is not None: + Validator = validates(version)(Validator) + + name = "{0}Validator".format(version.title().replace(" ", "")) + if not PY3 and isinstance(name, unicode): + name = name.encode("utf-8") + Validator.__name__ = name + + return Validator + + +def extend(validator, validators, version=None): + all_validators = dict(validator.VALIDATORS) + all_validators.update(validators) + return create( + meta_schema=validator.META_SCHEMA, + validators=all_validators, + version=version, + default_types=validator.DEFAULT_TYPES, + ) + + +Draft3Validator = create( + meta_schema=_utils.load_schema("draft3"), + validators={ + "$ref" : _validators.ref, + "additionalItems" : _validators.additionalItems, + "additionalProperties" : _validators.additionalProperties, + "dependencies" : _validators.dependencies, + "disallow" : _validators.disallow_draft3, + "divisibleBy" : _validators.multipleOf, + "enum" : _validators.enum, + "extends" : _validators.extends_draft3, + "format" : _validators.format, + "items" : _validators.items, + "maxItems" : _validators.maxItems, + "maxLength" : _validators.maxLength, + "maximum" : _validators.maximum, + "minItems" : _validators.minItems, + "minLength" : _validators.minLength, + "minimum" : _validators.minimum, + "multipleOf" : _validators.multipleOf, + "pattern" : _validators.pattern, + "patternProperties" : _validators.patternProperties, + "properties" : _validators.properties_draft3, + "type" : _validators.type_draft3, + "uniqueItems" : _validators.uniqueItems, + }, + version="draft3", +) + +Draft4Validator = create( + meta_schema=_utils.load_schema("draft4"), + validators={ + "$ref" : _validators.ref, + "additionalItems" : _validators.additionalItems, + "additionalProperties" : _validators.additionalProperties, + "allOf" : _validators.allOf_draft4, + "anyOf" : _validators.anyOf_draft4, + "dependencies" : _validators.dependencies, + "enum" : _validators.enum, + "format" : _validators.format, + "items" : _validators.items, + "maxItems" : _validators.maxItems, + "maxLength" : _validators.maxLength, + "maxProperties" : _validators.maxProperties_draft4, + "maximum" : _validators.maximum, + "minItems" : _validators.minItems, + "minLength" : _validators.minLength, + "minProperties" : _validators.minProperties_draft4, + "minimum" : _validators.minimum, + "multipleOf" : _validators.multipleOf, + "not" : _validators.not_draft4, + "oneOf" : _validators.oneOf_draft4, + "pattern" : _validators.pattern, + "patternProperties" : _validators.patternProperties, + "properties" : _validators.properties_draft4, + "required" : _validators.required_draft4, + "type" : _validators.type_draft4, + "uniqueItems" : _validators.uniqueItems, + }, + version="draft4", +) + + +class RefResolver(object): + """ + Resolve JSON References. + + :argument str base_uri: URI of the referring document + :argument referrer: the actual referring document + :argument dict store: a mapping from URIs to documents to cache + :argument bool cache_remote: whether remote refs should be cached after + first resolution + :argument dict handlers: a mapping from URI schemes to functions that + should be used to retrieve them + + """ + + def __init__( + self, base_uri, referrer, store=(), cache_remote=True, handlers=(), + ): + self.base_uri = base_uri + self.resolution_scope = base_uri + # This attribute is not used, it is for backwards compatibility + self.referrer = referrer + self.cache_remote = cache_remote + self.handlers = dict(handlers) + + self.store = _utils.URIDict( + (id, validator.META_SCHEMA) + for id, validator in iteritems(meta_schemas) + ) + self.store.update(store) + self.store[base_uri] = referrer + + @classmethod + def from_schema(cls, schema, *args, **kwargs): + """ + Construct a resolver from a JSON schema object. + + :argument schema schema: the referring schema + :rtype: :class:`RefResolver` + + """ + + return cls(schema.get("id", ""), schema, *args, **kwargs) + + @contextlib.contextmanager + def in_scope(self, scope): + old_scope = self.resolution_scope + self.resolution_scope = urljoin(old_scope, scope) + try: + yield + finally: + self.resolution_scope = old_scope + + @contextlib.contextmanager + def resolving(self, ref): + """ + Context manager which resolves a JSON ``ref`` and enters the + resolution scope of this ref. + + :argument str ref: reference to resolve + + """ + + full_uri = urljoin(self.resolution_scope, ref) + uri, fragment = urldefrag(full_uri) + if not uri: + uri = self.base_uri + + if uri in self.store: + document = self.store[uri] + else: + try: + document = self.resolve_remote(uri) + except Exception as exc: + raise RefResolutionError(exc) + + old_base_uri, self.base_uri = self.base_uri, uri + try: + with self.in_scope(uri): + yield self.resolve_fragment(document, fragment) + finally: + self.base_uri = old_base_uri + + def resolve_fragment(self, document, fragment): + """ + Resolve a ``fragment`` within the referenced ``document``. + + :argument document: the referrant document + :argument str fragment: a URI fragment to resolve within it + + """ + + fragment = fragment.lstrip("/") + parts = unquote(fragment).split("/") if fragment else [] + + for part in parts: + part = part.replace("~1", "/").replace("~0", "~") + + if isinstance(document, Sequence): + # Array indexes should be turned into integers + try: + part = int(part) + except ValueError: + pass + try: + document = document[part] + except (TypeError, LookupError): + raise RefResolutionError( + "Unresolvable JSON pointer: %r" % fragment + ) + + return document + + def resolve_remote(self, uri): + """ + Resolve a remote ``uri``. + + Does not check the store first, but stores the retrieved document in + the store if :attr:`RefResolver.cache_remote` is True. + + .. note:: + + If the requests_ library is present, ``jsonschema`` will use it to + request the remote ``uri``, so that the correct encoding is + detected and used. + + If it isn't, or if the scheme of the ``uri`` is not ``http`` or + ``https``, UTF-8 is assumed. + + :argument str uri: the URI to resolve + :returns: the retrieved document + + .. _requests: http://pypi.python.org/pypi/requests/ + + """ + + scheme = urlsplit(uri).scheme + + if scheme in self.handlers: + result = self.handlers[scheme](uri) + elif ( + scheme in ["http", "https"] and + requests and + getattr(requests.Response, "json", None) is not None + ): + # Requests has support for detecting the correct encoding of + # json over http + if callable(requests.Response.json): + result = requests.get(uri).json() + else: + result = requests.get(uri).json + else: + # Otherwise, pass off to urllib and assume utf-8 + result = json.loads(urlopen(uri).read().decode("utf-8")) + + if self.cache_remote: + self.store[uri] = result + return result + + +class ErrorTree(object): + """ + ErrorTrees make it easier to check which validations failed. + + """ + + _instance = _unset + + def __init__(self, errors=()): + self.errors = {} + self._contents = collections.defaultdict(self.__class__) + + for error in errors: + container = self + for element in error.path: + container = container[element] + container.errors[error.validator] = error + + self._instance = error.instance + + def __contains__(self, index): + """ + Check whether ``instance[index]`` has any errors. + + """ + + return index in self._contents + + def __getitem__(self, index): + """ + Retrieve the child tree one level down at the given ``index``. + + If the index is not in the instance that this tree corresponds to and + is not known by this tree, whatever error would be raised by + ``instance.__getitem__`` will be propagated (usually this is some + subclass of :class:`LookupError`. + + """ + + if self._instance is not _unset and index not in self: + self._instance[index] + return self._contents[index] + + def __setitem__(self, index, value): + self._contents[index] = value + + def __iter__(self): + """ + Iterate (non-recursively) over the indices in the instance with errors. + + """ + + return iter(self._contents) + + def __len__(self): + """ + Same as :attr:`total_errors`. + + """ + + return self.total_errors + + def __repr__(self): + return "<%s (%s total errors)>" % (self.__class__.__name__, len(self)) + + @property + def total_errors(self): + """ + The total number of errors in the entire tree, including children. + + """ + + child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) + return len(self.errors) + child_errors + + +def validator_for(schema, default=_unset): + if default is _unset: + default = Draft4Validator + return meta_schemas.get(schema.get("$schema", ""), default) + + +def validate(instance, schema, cls=None, *args, **kwargs): + """ + Validate an instance under the given schema. + + >>> validate([2, 3, 4], {"maxItems" : 2}) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + :func:`validate` will first verify that the provided schema is itself + valid, since not doing so can lead to less obvious error messages and fail + in less obvious or consistent ways. If you know you have a valid schema + already or don't care, you might prefer using the + :meth:`~IValidator.validate` method directly on a specific validator + (e.g. :meth:`Draft4Validator.validate`). + + + :argument instance: the instance to validate + :argument schema: the schema to validate with + :argument cls: an :class:`IValidator` class that will be used to validate + the instance. + + If the ``cls`` argument is not provided, two things will happen in + accordance with the specification. First, if the schema has a + :validator:`$schema` property containing a known meta-schema [#]_ then the + proper validator will be used. The specification recommends that all + schemas contain :validator:`$schema` properties for this reason. If no + :validator:`$schema` property is found, the default validator class is + :class:`Draft4Validator`. + + Any other provided positional and keyword arguments will be passed on when + instantiating the ``cls``. + + :raises: + :exc:`ValidationError` if the instance is invalid + + :exc:`SchemaError` if the schema itself is invalid + + .. rubric:: Footnotes + .. [#] known by a validator registered with :func:`validates` + """ + if cls is None: + cls = validator_for(schema) + cls.check_schema(schema) + cls(schema, *args, **kwargs).validate(instance) diff --git a/perftest b/perftest new file mode 100755 index 0000000..13933b4 --- /dev/null +++ b/perftest @@ -0,0 +1,46 @@ +#! /usr/bin/env python +""" +A *very* basic performance test. + +""" + +from __future__ import print_function +import argparse +import textwrap +import timeit + + +IMPORT = "from jsonschema import Draft3Validator, Draft4Validator, validate\n" + + +parser = argparse.ArgumentParser() +parser.add_argument("-n", "--number", type=int, default=100) +arguments = parser.parse_args() + + +print("Validating {0} times.".format(arguments.number)) + + +for name, benchmark in ( + ( + "Simple", """ + validator = Draft3Validator( + {"type" : "object", "properties" : {"foo" : {"required" : True}}} + ) + instance = {"foo" : 12, "bar" : 13} + """ + ), + ( + "Meta schema", """ + validator = Draft3Validator(Draft3Validator.META_SCHEMA) + instance = validator.META_SCHEMA + """ + ), +): + results = timeit.timeit( + number=arguments.number, + setup=IMPORT + textwrap.dedent(benchmark), + stmt="validator.validate(instance)", + ) + + print("{0:15}: {1} seconds".format(name, results)) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cd115e3 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +from distutils.core import setup + +from jsonschema import __version__ + + +with open("README.rst") as readme: + long_description = readme.read() + + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "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 :: 3.3", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + + +setup( + name="jsonschema", + version=__version__, + packages=["jsonschema", "jsonschema.tests"], + package_data={'jsonschema': ['schemas/*.json']}, + author="Julian Berman", + author_email="Julian@GrayVines.com", + classifiers=classifiers, + description="An implementation of JSON Schema validation for Python", + license="MIT", + long_description=long_description, + url="http://github.com/Julian/jsonschema", +) @@ -0,0 +1,62 @@ +[tox] +envlist = py26, py27, pypy, py32, py33, docs, style + +[testenv] +commands = + py.test -s jsonschema + {envpython} -m doctest README.rst +deps = + {[testenv:notpy33]deps} + {[testenv:py33]deps} + +[testenv:docs] +basepython = python +changedir = docs +deps = + lxml + sphinx +commands = + sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + +[testenv:style] +deps = flake8 +commands = + flake8 --max-complexity 10 jsonschema + +[testenv:py26] +deps = + {[testenv:notpy33]deps} + {[testenv:all]deps} + argparse + unittest2 + +[testenv:py33] +commands = + py.test -s jsonschema + {envpython} -m doctest README.rst + sphinx-build -b doctest docs {envtmpdir}/html +deps = + {[testenv:all]deps} + {[testenv:notpy26]deps} + +[testenv:notpy33] +deps = + mock + +[testenv:notpy26] +deps = + rfc3987 + +[testenv:all] +deps = + lxml + pytest + sphinx + strict-rfc3339 + webcolors + +[flake8] +ignore = E203,E302,E303,E701,F811 + +[pytest] +addopts = -r s |