Merge branch 'split_into_package'

Closes: #101
This commit is contained in:
Julian Berman
2013-05-12 23:52:08 -04:00
10 changed files with 943 additions and 711 deletions

25
jsonschema/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
"""
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._format import (
FormatChecker, FormatError, draft3_format_checker, draft4_format_checker,
)
from jsonschema.validators import (
RefResolutionError, SchemaError, ValidationError, UnknownType,
ErrorTree, Draft3Validator, Draft4Validator, RefResolver, ValidatorMixin,
validate, validates,
)
__version__ = "1.4.0-dev"
# flake8: noqa

215
jsonschema/_format.py Normal file
View File

@@ -0,0 +1,215 @@
import datetime
import re
import socket
from jsonschema.compat import PY3
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__
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 in self.checkers:
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 isodate
except ImportError:
pass
else:
_err = (ValueError, isodate.ISO8601Error)
_checks_drafts("date-time", raises=_err)(isodate.parse_datetime)
_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"])

207
jsonschema/_utils.py Normal file
View File

@@ -0,0 +1,207 @@
import itertools
import json
import re
import os
from jsonschema.compat import str_types, urlparse, MutableMapping
class URIDict(MutableMapping):
"""
Dictionary which uses normalized URIs as keys.
"""
def normalize(self, uri):
return urlparse.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)
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

30
jsonschema/compat.py Normal file
View File

@@ -0,0 +1,30 @@
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 import parse as urlparse
from urllib.parse import unquote
from urllib.request import urlopen
str_types = str,
int_types = int,
iteritems = operator.methodcaller("items")
else:
from itertools import izip as zip # noqa
import urlparse # noqa
from urllib import unquote # noqa
from urllib2 import urlopen # noqa
str_types = basestring
int_types = int, long
iteritems = operator.methodcaller("iteritems")
# flake8: noqa

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -1,56 +1,23 @@
"""
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 __future__ import division, unicode_literals
import collections
import contextlib
import datetime
import itertools
import json
import numbers
import operator
import pprint
import re
import socket
import sys
import textwrap
try:
from collections import MutableMapping, Sequence
except ImportError:
from collections.abc import MutableMapping, Sequence
try:
import requests
except ImportError:
requests = None
__version__ = "1.4.0-dev"
PY3 = sys.version_info[0] >= 3
if PY3:
from urllib import parse as urlparse
from urllib.parse import unquote
from urllib.request import urlopen
basestring = unicode = str
long = int
iteritems = operator.methodcaller("items")
else:
from itertools import izip as zip
from urllib import unquote
from urllib2 import urlopen
import urlparse
iteritems = operator.methodcaller("iteritems")
from jsonschema import _utils
from jsonschema.compat import (
PY3, Sequence, urlparse, unquote, urlopen, str_types, int_types, iteritems,
)
from jsonschema._format import FormatError
FLOAT_TOLERANCE = 10 ** -15
@@ -114,8 +81,8 @@ class _Error(Exception):
):
return self.message
path = _format_as_index(self.path)
schema_path = _format_as_index(list(self.schema_path)[:-1])
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)
@@ -130,70 +97,22 @@ class _Error(Exception):
) % (
self.validator,
schema_path,
_indent(pschema),
_utils.indent(pschema),
path,
_indent(pinstance),
_utils.indent(pinstance),
)
if PY3:
__str__ = __unicode__
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__
class SchemaError(_Error): pass
class ValidationError(_Error): pass
class RefResolutionError(Exception): pass
class UnknownType(Exception): pass
class _URIDict(MutableMapping):
"""
Dictionary which uses normalized URIs as keys.
"""
def normalize(self, uri):
return urlparse.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)
meta_schemas = _URIDict()
meta_schemas = _utils.URIDict()
def validates(version):
@@ -229,9 +148,9 @@ class ValidatorMixin(object):
"""
DEFAULT_TYPES = {
"array" : list, "boolean" : bool, "integer" : (int, long),
"array" : list, "boolean" : bool, "integer" : int_types,
"null" : type(None), "number" : numbers.Number, "object" : dict,
"string" : basestring,
"string" : str_types,
}
def __init__(self, schema, types=(), resolver=None, format_checker=None):
@@ -252,7 +171,7 @@ class ValidatorMixin(object):
# bool inherits from int, so ensure bools aren't reported as integers
if isinstance(instance, bool):
pytypes = _flatten(pytypes)
pytypes = _utils.flatten(pytypes)
num = any(issubclass(pytype, numbers.Number) for pytype in pytypes)
if num and bool not in pytypes:
return False
@@ -333,7 +252,7 @@ class _Draft34CommonMixin(object):
if not self.is_type(instance, "object"):
return
extras = set(_find_additional_properties(instance, schema))
extras = set(_utils.find_additional_properties(instance, schema))
if self.is_type(aP, "object"):
for extra in extras:
@@ -341,7 +260,7 @@ class _Draft34CommonMixin(object):
yield error
elif not aP and extras:
error = "Additional properties are not allowed (%s %s unexpected)"
yield ValidationError(error % _extras_msg(extras))
yield ValidationError(error % _utils.extras_msg(extras))
def validate_items(self, items, instance, schema):
if not self.is_type(instance, "array"):
@@ -373,7 +292,8 @@ class _Draft34CommonMixin(object):
elif not aI and len(instance) > len(schema.get("items", [])):
error = "Additional items are not allowed (%s %s unexpected)"
yield ValidationError(
error % _extras_msg(instance[len(schema.get("items", [])):])
error %
_utils.extras_msg(instance[len(schema.get("items", [])):])
)
def validate_minimum(self, minimum, instance, schema):
@@ -434,7 +354,11 @@ class _Draft34CommonMixin(object):
yield ValidationError("%r is too long" % (instance,))
def validate_uniqueItems(self, uI, instance, schema):
if uI and self.is_type(instance, "array") and not _uniq(instance):
if (
uI and
self.is_type(instance, "array") and
not _utils.uniq(instance)
):
yield ValidationError("%r has non-unique elements" % instance)
def validate_pattern(self, patrn, instance, schema):
@@ -473,7 +397,7 @@ class _Draft34CommonMixin(object):
):
yield error
else:
dependencies = _list(dependency)
dependencies = _utils.ensure_list(dependency)
for dependency in dependencies:
if dependency not in instance:
yield ValidationError(
@@ -498,7 +422,7 @@ class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object):
"""
def validate_type(self, types, instance, schema):
types = _list(types)
types = _utils.ensure_list(types)
all_errors = []
for index, type in enumerate(types):
@@ -514,7 +438,7 @@ class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object):
return
else:
yield ValidationError(
_types_msg(instance, types), context=all_errors,
_utils.types_msg(instance, types), context=all_errors,
)
def validate_properties(self, properties, instance, schema):
@@ -543,7 +467,7 @@ class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object):
yield error
def validate_disallow(self, disallow, instance, schema):
for disallowed in _list(disallow):
for disallowed in _utils.ensure_list(disallow):
if self.is_valid(instance, {"type" : [disallowed]}):
yield ValidationError(
"%r is disallowed for %r" % (disallowed, instance)
@@ -560,88 +484,7 @@ class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object):
validate_divisibleBy = _Draft34CommonMixin._validate_multipleOf
META_SCHEMA = {
"$schema" : "http://json-schema.org/draft-03/schema#",
"id" : "http://json-schema.org/draft-03/schema#",
"type" : "object",
"properties" : {
"type" : {
"type" : ["string", "array"],
"items" : {"type" : ["string", {"$ref" : "#"}]},
"uniqueItems" : True,
"default" : "any"
},
"properties" : {
"type" : "object",
"additionalProperties" : {"$ref" : "#", "type": "object"},
"default" : {}
},
"patternProperties" : {
"type" : "object",
"additionalProperties" : {"$ref" : "#"},
"default" : {}
},
"additionalProperties" : {
"type" : [{"$ref" : "#"}, "boolean"], "default" : {}
},
"items" : {
"type" : [{"$ref" : "#"}, "array"],
"items" : {"$ref" : "#"},
"default" : {}
},
"additionalItems" : {
"type" : [{"$ref" : "#"}, "boolean"], "default" : {}
},
"required" : {"type" : "boolean", "default" : False},
"dependencies" : {
"type" : ["string", "array", "object"],
"additionalProperties" : {
"type" : ["string", "array", {"$ref" : "#"}],
"items" : {"type" : "string"}
},
"default" : {}
},
"minimum" : {"type" : "number"},
"maximum" : {"type" : "number"},
"exclusiveMinimum" : {"type" : "boolean", "default" : False},
"exclusiveMaximum" : {"type" : "boolean", "default" : False},
"minItems" : {"type" : "integer", "minimum" : 0, "default" : 0},
"maxItems" : {"type" : "integer", "minimum" : 0},
"uniqueItems" : {"type" : "boolean", "default" : False},
"pattern" : {"type" : "string", "format" : "regex"},
"minLength" : {"type" : "integer", "minimum" : 0, "default" : 0},
"maxLength" : {"type" : "integer"},
"enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True},
"default" : {"type" : "any"},
"title" : {"type" : "string"},
"description" : {"type" : "string"},
"format" : {"type" : "string"},
"maxDecimal" : {"type" : "number", "minimum" : 0},
"divisibleBy" : {
"type" : "number",
"minimum" : 0,
"exclusiveMinimum" : True,
"default" : 1
},
"disallow" : {
"type" : ["string", "array"],
"items" : {"type" : ["string", {"$ref" : "#"}]},
"uniqueItems" : True
},
"extends" : {
"type" : [{"$ref" : "#"}, "array"],
"items" : {"$ref" : "#"},
"default" : {}
},
"id" : {"type" : "string", "format" : "uri"},
"$ref" : {"type" : "string", "format" : "uri"},
"$schema" : {"type" : "string", "format" : "uri"},
},
"dependencies" : {
"exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum"
},
}
META_SCHEMA = _utils.load_schema('draft3')
@validates("draft4")
@@ -652,10 +495,10 @@ class Draft4Validator(ValidatorMixin, _Draft34CommonMixin, object):
"""
def validate_type(self, types, instance, schema):
types = _list(types)
types = _utils.ensure_list(types)
if not any(self.is_type(instance, type) for type in types):
yield ValidationError(_types_msg(instance, types))
yield ValidationError(_utils.types_msg(instance, types))
def validate_properties(self, properties, instance, schema):
if not self.is_type(instance, "object"):
@@ -737,362 +580,7 @@ class Draft4Validator(ValidatorMixin, _Draft34CommonMixin, object):
validate_multipleOf = _Draft34CommonMixin._validate_multipleOf
META_SCHEMA = {
"id": "http://json-schema.org/draft-04/schema#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#"}
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [
{"$ref": "#/definitions/positiveInteger"}, {"default": 0}
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string",
]
},
"stringArray": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"uniqueItems": True
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": True
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": False
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": False
},
"maxLength": {"$ref": "#/definitions/positiveInteger"},
"minLength": {"$ref": "#/definitions/positiveIntegerDefault0"},
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{"type": "boolean"},
{"$ref": "#"}
],
"default": {}
},
"items": {
"anyOf": [
{"$ref": "#"},
{"$ref": "#/definitions/schemaArray"}
],
"default": {}
},
"maxItems": {"$ref": "#/definitions/positiveInteger"},
"minItems": {"$ref": "#/definitions/positiveIntegerDefault0"},
"uniqueItems": {
"type": "boolean",
"default": False
},
"maxProperties": {"$ref": "#/definitions/positiveInteger"},
"minProperties": {"$ref": "#/definitions/positiveIntegerDefault0"},
"required": {"$ref": "#/definitions/stringArray"},
"additionalProperties": {
"anyOf": [
{"type": "boolean"},
{"$ref": "#"}
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": {"$ref": "#"},
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": {"$ref": "#"},
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": {"$ref": "#"},
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{"$ref": "#"},
{"$ref": "#/definitions/stringArray"}
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": True
},
"type": {
"anyOf": [
{"$ref": "#/definitions/simpleTypes"},
{
"type": "array",
"items": {"$ref": "#/definitions/simpleTypes"},
"minItems": 1,
"uniqueItems": True
}
]
},
"allOf": {"$ref": "#/definitions/schemaArray"},
"anyOf": {"$ref": "#/definitions/schemaArray"},
"oneOf": {"$ref": "#/definitions/schemaArray"},
"not": {"$ref": "#"}
},
"dependencies": {
"exclusiveMaximum": ["maximum"],
"exclusiveMinimum": ["minimum"]
},
"default": {}
}
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.
>>> checker = FormatChecker(formats=("date-time", "regex"))
"""
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 in self.checkers:
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 isodate
except ImportError:
pass
else:
_err = (ValueError, isodate.ISO8601Error)
_checks_drafts("date-time", raises=_err)(isodate.parse_datetime)
_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"])
META_SCHEMA = _utils.load_schema('draft4')
class RefResolver(object):
@@ -1119,7 +607,7 @@ class RefResolver(object):
self.cache_remote = cache_remote
self.handlers = dict(handlers)
self.store = _URIDict(
self.store = _utils.URIDict(
(id, validator.META_SCHEMA)
for id, validator in iteritems(meta_schemas)
)
@@ -1315,161 +803,6 @@ class ErrorTree(object):
return len(self.errors) + child_errors
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 _list(thing):
"""
Wrap ``thing`` in a list if it's a single str.
Otherwise, return it unchanged.
"""
if isinstance(thing, basestring):
return [thing]
return thing
def _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
def validate(instance, schema, cls=None, *args, **kwargs):
if cls is None:
cls = meta_schemas.get(schema.get("$schema", ""), Draft4Validator)

View File

@@ -28,7 +28,8 @@ classifiers = [
setup(
name="jsonschema",
version=__version__,
py_modules=["jsonschema"],
packages=["jsonschema"],
package_data={'jsonschema': ['schemas/*.json']},
author="Julian Berman",
author_email="Julian@GrayVines.com",
classifiers=classifiers,

View File

@@ -28,11 +28,12 @@ except ImportError:
pypy_version_info = None
from jsonschema import (
PY3, FormatError, RefResolutionError, SchemaError, UnknownType,
FormatError, RefResolutionError, SchemaError, UnknownType,
ValidationError, ErrorTree, Draft3Validator, Draft4Validator,
FormatChecker, RefResolver, ValidatorMixin, draft3_format_checker,
draft4_format_checker, validate,
)
from jsonschema.compat import PY3
THIS_DIR = os.path.dirname(__file__)
@@ -230,7 +231,7 @@ class TestDraft4(
class RemoteRefResolutionMixin(object):
def setUp(self):
patch = mock.patch("jsonschema.requests")
patch = mock.patch("jsonschema.validators.requests")
requests = patch.start()
requests.get.side_effect = self.resolve
self.addCleanup(patch.stop)
@@ -879,7 +880,7 @@ class TestRefResolver(unittest.TestCase):
ref = "http://bar#baz"
schema = {"baz" : 12}
with mock.patch("jsonschema.requests") as requests:
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)
@@ -889,8 +890,8 @@ class TestRefResolver(unittest.TestCase):
ref = "http://bar#baz"
schema = {"baz" : 12}
with mock.patch("jsonschema.requests", None):
with mock.patch("jsonschema.urlopen") as urlopen:
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:

12
tox.ini
View File

@@ -1,11 +1,10 @@
[tox]
envlist = py26, py27, pypy, py32, py33, docs
envlist = py26, py27, pypy, py32, py33, docs, style
[testenv]
commands =
py.test -s tests.py
py.test -s test_jsonschema.py
{envpython} -m doctest README.rst
{envpython} -m doctest jsonschema.py
deps =
{[testenv:notpy33]deps}
{[testenv:py33]deps}
@@ -22,8 +21,8 @@ commands =
[testenv:style]
deps = flake8
commands =
flake8 --max-complexity 10 jsonschema.py
flake8 tests.py
flake8 --max-complexity 10 jsonschema
flake8 test_jsonschema.py
[flake8]
ignore = E203,E302,E303,E701,F811
@@ -37,9 +36,8 @@ deps =
[testenv:py33]
commands =
py.test -s tests.py
py.test -s test_jsonschema.py
{envpython} -m doctest README.rst
{envpython} -m doctest jsonschema.py
sphinx-build -b doctest docs {envtmpdir}/html
deps =
{[testenv:all]deps}