Add a JSON Schema documentation role.

This commit is contained in:
Julian Berman
2013-04-17 23:39:52 -04:00
parent cc09cafc1d
commit ac9b9c624e
7 changed files with 197 additions and 71 deletions

4
.gitignore vendored
View File

@@ -14,6 +14,10 @@ MANIFEST
coverage coverage
htmlcov htmlcov
_cache
_static
_templates
_trial_temp _trial_temp
.tox .tox

View File

@@ -23,8 +23,11 @@ extensions = [
'sphinx.ext.doctest', 'sphinx.ext.doctest',
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'jsonschema_role',
] ]
cache_path = "_cache"
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -58,7 +61,7 @@ version = release.partition("-")[0]
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ['_build', "_cache", "_static", "_templates"]
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None #default_role = None

View File

@@ -17,5 +17,6 @@ Creating Validation Errors
Any validating function that validates against a subschema should call Any validating function that validates against a subschema should call
:meth:`ValidatorMixin.descend`, rather than :meth:`ValidatorMixin.iter_errors`. :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 If it recurses into the instance, or schema, it should pass one or both of the
``path`` or ``schema_path`` arguments to ``descend`` in order to properly ``path`` or ``schema_path`` arguments to :meth:`ValidatorMixin.descend` in
maintain where in the instance or schema respsectively the error occurred. order to properly maintain where in the instance or schema respsectively the
error occurred.

View File

@@ -0,0 +1 @@
lxml

View File

@@ -17,7 +17,8 @@ raised or returned, depending on which method or function is used.
.. attribute:: validator .. attribute:: validator
The failed validator. The failed `validator
<http://json-schema.org/latest/json-schema-validation.html#anchor12>`_.
.. attribute:: validator_value .. attribute:: validator_value
@@ -27,7 +28,7 @@ raised or returned, depending on which method or function is used.
The full schema that this error came from. This is potentially a The full schema that this error came from. This is potentially a
subschema from within the schema that was passed into the validator, or subschema from within the schema that was passed into the validator, or
even an entirely different schema if a ``$ref`` was followed. even an entirely different schema if a :validator:`$ref` was followed.
.. attribute:: schema_path .. attribute:: schema_path
@@ -53,8 +54,8 @@ raised or returned, depending on which method or function is used.
If the error was caused by errors in subschemas, the list of errors If the error was caused by errors in subschemas, the list of errors
from the subschemas will be available on this property. The from the subschemas will be available on this property. The
``schema_path`` and ``path`` of these errors will be relative to the :attr:`.schema_path` and :attr:`.path` of these errors will be relative
parent error. to the parent error.
.. attribute:: cause .. attribute:: cause
@@ -99,9 +100,9 @@ The error messages in this situation are not very helpful on their own:
The instance is not valid under any of the given schemas The instance is not valid under any of the given schemas
The instance is not valid under any of the given schemas The instance is not valid under any of the given schemas
If we look at :attr:`ValidationError.path` on each of the errors, we can find 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 out which elements in the instance correspond to each of the errors. In
this example, :attr:`ValidationError.path` will have only one element, which this example, :attr:`~ValidationError.path` will have only one element, which
will be the index in our list. will be the index in our list.
.. code-block:: python .. code-block:: python
@@ -114,16 +115,16 @@ will be the index in our list.
Since our schema contained nested subschemas, it can be helpful to look at 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. the specific part of the instance and subschema that caused each of the errors.
This can be seen with the :attr:`ValidationError.instance` and This can be seen with the :attr:`~ValidationError.instance` and
:attr:`ValidationError.schema` attributes. :attr:`~ValidationError.schema` attributes.
With validators like ``anyOf``, the :attr:`ValidationError.context` attribute With validators like :validator:`anyOf`, the :attr:`~ValidationError.context`
can be used to see the sub-errors which caused the failure. Since these errors attribute can be used to see the sub-errors which caused the failure. Since
actually came from two separate subschemas, it can be helpful to look at the these errors actually came from two separate subschemas, it can be helpful to
:attr:`ValidationError.schema_path` attribute as well to see where exactly in look at the :attr:`~ValidationError.schema_path` attribute as well to see where
the schema each of these errors come from. In the case of sub-errors from the exactly in the schema each of these errors come from. In the case of sub-errors
:attr:`ValidationError.context` attribute, this path will be relative to the from the :attr:`~ValidationError.context` attribute, this path will be relative
:attr:`ValidationError.schema_path` of the parent error. to the :attr:`~ValidationError.schema_path` of the parent error.
.. code-block:: python .. code-block:: python
@@ -180,11 +181,11 @@ more easily than by just iterating over the error objects.
As you can see, :class:`ErrorTree` takes an iterable of As you can see, :class:`ErrorTree` takes an iterable of
:class:`ValidationError`\s when constructing a tree so you can directly pass it :class:`ValidationError`\s when constructing a tree so you can directly pass it
the return value of a validator's ``iter_errors`` method. 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 :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 might want to perform is to check whether a given element in our instance
failed validation. We do so using the ``in`` operator: failed validation. We do so using the :keyword:`in` operator:
.. code-block:: python .. code-block:: python
@@ -199,16 +200,17 @@ did have an error (in fact it had 2), while the 1th index (``2``) did not (i.e.
it was valid). it was valid).
If we want to see which errors a child had, we index into the tree and look at If we want to see which errors a child had, we index into the tree and look at
the ``errors`` attribute. the :attr:`~ErrorTree.errors` attribute.
.. code-block:: python .. code-block:: python
>>> sorted(tree[0].errors) >>> sorted(tree[0].errors)
['enum', 'type'] ['enum', 'type']
Here we see that the ``enum`` and ``type`` validators failed for index 0. In Here we see that the :validator:`enum` and :validator:`type` validators failed
fact ``errors`` is a dict, whose values are the :class:`ValidationError`\s, so for index ``0``. In fact :attr:`~ErrorTree.errors` is a dict, whose values are
we can get at those directly if we want them. the :class:`ValidationError`\s, so we can get at those directly if we want
them.
.. code-block:: python .. code-block:: python
@@ -216,7 +218,7 @@ we can get at those directly if we want them.
'spam' is not of type 'number' 'spam' is not of type 'number'
Of course this means that if we want to know if a given validator failed for a 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 ``errors``: given index, we check for its presence in :attr:`~ErrorTree.errors`:
.. code-block:: python .. code-block:: python
@@ -227,9 +229,9 @@ given index, we check for its presence in ``errors``:
False False
Finally, if you were paying close enough attention, you'll notice that we Finally, if you were paying close enough attention, you'll notice that we
haven't seen our ``minItems`` error appear anywhere yet. This is because haven't seen our :validator:`minItems` error appear anywhere yet. This is
``minItems`` is an error that applies globally to the instance itself. So it because :validator:`minItems` is an error that applies globally to the instance
appears in the root node of the tree. itself. So it appears in the root node of the tree.
.. code-block:: python .. code-block:: python
@@ -240,5 +242,5 @@ That's all you need to know to use error trees.
To summarize, each tree contains child trees that can be accessed by indexing 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 the tree to get the corresponding child tree for a given index into the
instance. Each tree and child has a ``errors`` attribute, a dict, that maps the instance. Each tree and child has a :attr:`~ErrorTree.errors` attribute, a
failed validator to the corresponding validation error. dict, that maps the failed validator to the corresponding validation error.

111
docs/jsonschema_role.py Normal file
View File

@@ -0,0 +1,111 @@
from datetime import datetime
from docutils import nodes
import errno
import os
import urllib2
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 = urllib2.Request(VALIDATION_SPEC, headers=headers)
response = urllib2.urlopen(request)
if response.code == 200:
with open(spec_path, "w+") 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
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
"""
header = spec.xpath(
"//h3[re:match(text(), '(^|\W){0}($|\W,)', 'i')]".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.warning(
"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

View File

@@ -14,7 +14,7 @@ The simplest way to validate an instance under a given schema is to use the
.. autofunction:: validate .. autofunction:: validate
Validate an ``instance`` under the given ``schema``. Validate an instance under the given schema.
>>> validate([2, 3, 4], {"maxItems" : 2}) >>> validate([2, 3, 4], {"maxItems" : 2})
Traceback (most recent call last): Traceback (most recent call last):
@@ -24,8 +24,9 @@ The simplest way to validate an instance under a given schema is to use the
:func:`validate` will first verify that the provided schema is itself :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 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 in less obvious or consistent ways. If you know you have a valid schema
already or don't care, you might prefer using the ``validate`` method already or don't care, you might prefer using the
directly on a specific validator (e.g. :meth:`Draft4Validator.validate`). :meth:`~IValidator.validate` method directly on a specific validator
(e.g. :meth:`Draft4Validator.validate`).
:argument instance: the instance to validate :argument instance: the instance to validate
@@ -34,12 +35,12 @@ The simplest way to validate an instance under a given schema is to use the
the instance. the instance.
If the ``cls`` argument is not provided, two things will happen in If the ``cls`` argument is not provided, two things will happen in
accordance with the specification. First, if the ``schema`` has a accordance with the specification. First, if the schema has a
``$schema`` property containing a known meta-schema (known by a validator :validator:`$schema` property containing a known meta-schema [#]_ then the
registered with :func:`validates`), then the proper validator will be used. proper validator will be used. The specification recommends that all
The specification recommends that all schemas contain ``$schema`` schemas contain :validator:`$schema` properties for this reason. If no
properties for this reason. If no ``$schema`` property is found, the :validator:`$schema` property is found, the default validator class is
default validator class is :class:`Draft4Validator`. :class:`Draft4Validator`.
Any other provided positional and keyword arguments will be passed on when Any other provided positional and keyword arguments will be passed on when
instantiating the ``cls``. instantiating the ``cls``.
@@ -49,6 +50,9 @@ The simplest way to validate an instance under a given schema is to use the
:exc:`SchemaError` if the schema itself is invalid :exc:`SchemaError` if the schema itself is invalid
.. rubric:: Footnotes
.. [#] known by a validator registered with :func:`validates`
The Validator Interface The Validator Interface
----------------------- -----------------------
@@ -64,25 +68,25 @@ adhere to.
:meth:`IValidator.check_schema` to validate a schema :meth:`IValidator.check_schema` to validate a schema
first. first.
:argument types: Override or extend the list of known types when validating :argument types: Override or extend the list of known types when validating
the ``type`` property. Should map strings (type names) to the :validator:`type` property. Should map strings (type
class objects that will be checked via ``isinstance``. See names) to class objects that will be checked via
:ref:`validating-types` for details. :func:`isinstance`. See :ref:`validating-types` for
details.
:type types: dict or iterable of 2-tuples :type types: dict or iterable of 2-tuples
:argument resolver: an instance of :class:`RefResolver` that will be used :argument resolver: an instance of :class:`RefResolver` that will be used
to resolve ``$ref`` properties (JSON references). If to resolve :validator:`$ref` properties (JSON
unprovided, one will be created. references). If unprovided, one will be created.
:argument format_checker: an object with a ``conform()`` method that will :argument format_checker: an instance of :class:`FormatChecker` whose
be called to check and see if instances conform :meth:`~conforms` method will be called to check
to each ``format`` property present in the and see if instances conform to each
:validator:`format` property present in the
schema. If unprovided, no validation will be done schema. If unprovided, no validation will be done
for ``format``. :class:`FormatChecker` is a for :validator:`format`.
concrete implementation of an object of this form
that can be used for common formats.
.. attribute:: DEFAULT_TYPES .. attribute:: DEFAULT_TYPES
The default mapping of JSON types to Python types used when validating The default mapping of JSON types to Python types used when validating
``type`` properties in JSON schemas. :validator:`type` properties in JSON schemas.
.. attribute:: META_SCHEMA .. attribute:: META_SCHEMA
@@ -162,20 +166,21 @@ Validating With Additional Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Occasionally it can be useful to provide additional or alternate types when Occasionally it can be useful to provide additional or alternate types when
validating the JSON Schema's ``type`` property. Validators allow this by taking validating the JSON Schema's :validator:`type` property. Validators allow this
a ``types`` argument on construction that specifies additional types, or which by taking a ``types`` argument on construction that specifies additional types,
can be used to specify a different set of Python types to map to a given JSON or which can be used to specify a different set of Python types to map to a
type. given JSON type.
``jsonschema`` tries to strike a balance between performance in the common case :mod:`jsonschema` tries to strike a balance between performance in the common
and generality. For instance, JSON defines a ``number`` type, which can be case and generality. For instance, JSON Schema defines a ``number`` type, which
validated with a schema such as ``{"type" : "number"}``. By default, this will can be validated with a schema such as ``{"type" : "number"}``. By default,
accept instances of Python :class:`number.Number`. This includes in particular this will accept instances of Python :class:`number.Number`. This includes in
:class:`int`\s and :class:`float`\s, along with `decimal.Decimal` objects, particular :class:`int`\s and :class:`float`\s, along with `decimal.Decimal`
:class:`complex` numbers etc. See the numbers_ module documentation for more objects, :class:`complex` numbers etc. For ``integer`` and ``object``, however,
details. For ``integer`` and ``object``, however, rather than checking for rather than checking for :class:`number.Integral` and
``number.Integral`` and ``collections.Mapping``, ``jsonschema`` simply checks :class:`collections.Mapping`, :mod:`jsonschema` simply checks for :class:`int`
for ``int`` and ``dict``, since the former can introduce significant slowdown. and :class:`dict`, since the former can introduce significant slowdown in these
common cases.
If you *do* want the generality, or just want to add a few specific additional 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 as being acceptible for a validator, :class:`IValidator`\s have a
@@ -197,8 +202,6 @@ 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 types, so you may want to access the set of default types when specifying your
additional type. additional type.
.. _numbers: http://docs.python.org/3.3/library/numbers.html
.. _versioned-validators: .. _versioned-validators:
Versioned Validators Versioned Validators
@@ -217,10 +220,11 @@ implements.
Validating Formats Validating Formats
------------------ ------------------
JSON Schema defines the ``format`` property which can be used to check if JSON Schema defines the :validator:`format` property which can be used to check
primitive types (``str``\s, ``number``\s, ``bool``\s) conform to well-defined if primitive types (``string``\s, ``number``\s, ``boolean``\s) conform to
formats. By default, no validation is enforced, but optionally, validation can well-defined formats. By default, no validation is enforced, but optionally,
be enabled by hooking in a format-checking object into an :class:`IValidator`. validation can be enabled by hooking in a format-checking object into an
:class:`IValidator`.
.. doctest:: .. doctest::
@@ -270,7 +274,7 @@ Checker Notes
========== ==================== ========== ====================
hostname hostname
ipv4 ipv4
ipv6 OS must have ``socket.inet_pton`` function ipv6 OS must have :func:`socket.inet_pton` function
email email
uri requires rfc3987_ uri requires rfc3987_
date-time requires isodate_ date-time requires isodate_