diff options
author | Julian Berman <Julian@GrayVines.com> | 2013-05-12 23:52:08 -0400 |
---|---|---|
committer | Julian Berman <Julian@GrayVines.com> | 2013-05-12 23:53:04 -0400 |
commit | fc75480f092dfcac2cf28e586c2b0478062ea12f (patch) | |
tree | 265fdd2ff10dea7779edd6b0aeba5a8b01d6d75b | |
parent | d8fd3f8bd2f60daec80040972a9d55a1eae1bc42 (diff) | |
parent | caca66735074820133ec754cb834aa38628a9b35 (diff) | |
download | jsonschema-fc75480f092dfcac2cf28e586c2b0478062ea12f.tar.gz |
Merge branch 'split_into_package'
Closes: #101
-rw-r--r-- | jsonschema/__init__.py | 25 | ||||
-rw-r--r-- | jsonschema/_format.py | 215 | ||||
-rw-r--r-- | jsonschema/_utils.py | 207 | ||||
-rw-r--r-- | jsonschema/compat.py | 30 | ||||
-rw-r--r-- | jsonschema/schemas/draft3.json | 201 | ||||
-rw-r--r-- | jsonschema/schemas/draft4.json | 221 | ||||
-rw-r--r-- | jsonschema/validators.py (renamed from jsonschema.py) | 729 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | test_jsonschema.py (renamed from tests.py) | 11 | ||||
-rw-r--r-- | tox.ini | 12 |
10 files changed, 943 insertions, 711 deletions
diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py new file mode 100644 index 0000000..adf9e2d --- /dev/null +++ b/jsonschema/__init__.py @@ -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 diff --git a/jsonschema/_format.py b/jsonschema/_format.py new file mode 100644 index 0000000..238e7117 --- /dev/null +++ b/jsonschema/_format.py @@ -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"]) diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py new file mode 100644 index 0000000..10e7bf8 --- /dev/null +++ b/jsonschema/_utils.py @@ -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 diff --git a/jsonschema/compat.py b/jsonschema/compat.py new file mode 100644 index 0000000..44c6ede --- /dev/null +++ b/jsonschema/compat.py @@ -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 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.py b/jsonschema/validators.py index 2f4dab5..9741125 100644 --- a/jsonschema.py +++ b/jsonschema/validators.py @@ -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) @@ -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, diff --git a/tests.py b/test_jsonschema.py index 7d7d9dc..11d8cd8 100644 --- a/tests.py +++ b/test_jsonschema.py @@ -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: @@ -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} |