summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore25
-rw-r--r--.travis.yml13
-rw-r--r--CHANGELOG.rst114
-rw-r--r--COPYING19
-rw-r--r--MANIFEST.in4
-rw-r--r--README.rst106
-rw-r--r--docs/Makefile153
-rw-r--r--docs/conf.py236
-rw-r--r--docs/creating.rst91
-rw-r--r--docs/doc-requirements.txt1
-rw-r--r--docs/errors.rst303
-rw-r--r--docs/faq.rst94
-rw-r--r--docs/index.rst52
-rw-r--r--docs/jsonschema_role.py123
-rw-r--r--docs/make.bat190
-rw-r--r--docs/references.rst13
-rw-r--r--docs/validate.rst264
-rw-r--r--json/.gitignore1
-rw-r--r--json/.travis.yml4
-rw-r--r--json/LICENSE (renamed from LICENSE)0
-rw-r--r--json/README.md (renamed from README.md)0
-rwxr-xr-xjson/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__.py26
-rw-r--r--jsonschema/_format.py206
-rw-r--r--jsonschema/_utils.py217
-rw-r--r--jsonschema/_validators.py363
-rw-r--r--jsonschema/compat.py51
-rw-r--r--jsonschema/exceptions.py112
-rw-r--r--jsonschema/schemas/draft3.json201
-rw-r--r--jsonschema/schemas/draft4.json221
-rw-r--r--jsonschema/tests/__init__.py0
-rw-r--r--jsonschema/tests/compat.py15
-rw-r--r--jsonschema/tests/test_format.py63
-rw-r--r--jsonschema/tests/test_jsonschema_test_suite.py249
-rw-r--r--jsonschema/tests/test_validators.py847
-rw-r--r--jsonschema/validators.py508
-rwxr-xr-xperftest46
-rw-r--r--setup.py40
-rw-r--r--tox.ini62
98 files changed, 5030 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index 1333ed7..4ea7454 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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.
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..af9cfbd
--- /dev/null
+++ b/COPYING
@@ -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/LICENSE b/json/LICENSE
index c28adba..c28adba 100644
--- a/LICENSE
+++ b/json/LICENSE
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",
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..05dbbbf
--- /dev/null
+++ b/tox.ini
@@ -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