Show More
@@ -0,0 +1,222 | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | # | |
|
3 | # python-json-pointer - An implementation of the JSON Pointer syntax | |
|
4 | # https://github.com/stefankoegl/python-json-pointer | |
|
5 | # | |
|
6 | # Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net> | |
|
7 | # All rights reserved. | |
|
8 | # | |
|
9 | # Redistribution and use in source and binary forms, with or without | |
|
10 | # modification, are permitted provided that the following conditions | |
|
11 | # are met: | |
|
12 | # | |
|
13 | # 1. Redistributions of source code must retain the above copyright | |
|
14 | # notice, this list of conditions and the following disclaimer. | |
|
15 | # 2. Redistributions in binary form must reproduce the above copyright | |
|
16 | # notice, this list of conditions and the following disclaimer in the | |
|
17 | # documentation and/or other materials provided with the distribution. | |
|
18 | # 3. The name of the author may not be used to endorse or promote products | |
|
19 | # derived from this software without specific prior written permission. | |
|
20 | # | |
|
21 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR | |
|
22 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |
|
23 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | |
|
24 | # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, | |
|
25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | |
|
26 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
|
27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
|
28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
|
29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | |
|
30 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
|
31 | # | |
|
32 | ||
|
33 | """ Identify specific nodes in a JSON document according to | |
|
34 | http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04 """ | |
|
35 | ||
|
36 | # Will be parsed by setup.py to determine package metadata | |
|
37 | __author__ = 'Stefan K�gl <stefan@skoegl.net>' | |
|
38 | __version__ = '0.3' | |
|
39 | __website__ = 'https://github.com/stefankoegl/python-json-pointer' | |
|
40 | __license__ = 'Modified BSD License' | |
|
41 | ||
|
42 | ||
|
43 | import urllib | |
|
44 | from itertools import tee, izip | |
|
45 | ||
|
46 | ||
|
47 | class JsonPointerException(Exception): | |
|
48 | pass | |
|
49 | ||
|
50 | ||
|
51 | _nothing = object() | |
|
52 | ||
|
53 | ||
|
54 | def resolve_pointer(doc, pointer, default=_nothing): | |
|
55 | """ | |
|
56 | Resolves pointer against doc and returns the referenced object | |
|
57 | ||
|
58 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} | |
|
59 | ||
|
60 | >>> resolve_pointer(obj, '') == obj | |
|
61 | True | |
|
62 | ||
|
63 | >>> resolve_pointer(obj, '/foo') == obj['foo'] | |
|
64 | True | |
|
65 | ||
|
66 | >>> resolve_pointer(obj, '/foo/another%20prop') == obj['foo']['another prop'] | |
|
67 | True | |
|
68 | ||
|
69 | >>> resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz'] | |
|
70 | True | |
|
71 | ||
|
72 | >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0] | |
|
73 | True | |
|
74 | ||
|
75 | >>> resolve_pointer(obj, '/some/path', None) == None | |
|
76 | True | |
|
77 | ||
|
78 | """ | |
|
79 | ||
|
80 | pointer = JsonPointer(pointer) | |
|
81 | return pointer.resolve(doc, default) | |
|
82 | ||
|
83 | ||
|
84 | def set_pointer(doc, pointer, value): | |
|
85 | """ | |
|
86 | Set a field to a given value | |
|
87 | ||
|
88 | The field is indicates by a base location that is given in the constructor, | |
|
89 | and an optional relative location in the call to set. If the path doesn't | |
|
90 | exist, it is created if possible | |
|
91 | ||
|
92 | >>> obj = {"foo": 2} | |
|
93 | >>> pointer = JsonPointer('/bar') | |
|
94 | >>> pointer.set(obj, 'one', '0') | |
|
95 | >>> pointer.set(obj, 'two', '1') | |
|
96 | >>> obj | |
|
97 | {'foo': 2, 'bar': ['one', 'two']} | |
|
98 | ||
|
99 | >>> obj = {"foo": 2, "bar": []} | |
|
100 | >>> pointer = JsonPointer('/bar') | |
|
101 | >>> pointer.set(obj, 5, '0/x') | |
|
102 | >>> obj | |
|
103 | {'foo': 2, 'bar': [{'x': 5}]} | |
|
104 | ||
|
105 | >>> obj = {'foo': 2, 'bar': [{'x': 5}]} | |
|
106 | >>> pointer = JsonPointer('/bar/0') | |
|
107 | >>> pointer.set(obj, 10, 'y/0') | |
|
108 | >>> obj | |
|
109 | {'foo': 2, 'bar': [{'y': [10], 'x': 5}]} | |
|
110 | """ | |
|
111 | ||
|
112 | pointer = JsonPointer(pointer) | |
|
113 | pointer.set(doc, value) | |
|
114 | ||
|
115 | ||
|
116 | class JsonPointer(object): | |
|
117 | """ A JSON Pointer that can reference parts of an JSON document """ | |
|
118 | ||
|
119 | def __init__(self, pointer): | |
|
120 | parts = pointer.split('/') | |
|
121 | if parts.pop(0) != '': | |
|
122 | raise JsonPointerException('location must starts with /') | |
|
123 | ||
|
124 | parts = map(urllib.unquote, parts) | |
|
125 | parts = [part.replace('~1', '/') for part in parts] | |
|
126 | parts = [part.replace('~0', '~') for part in parts] | |
|
127 | self.parts = parts | |
|
128 | ||
|
129 | ||
|
130 | ||
|
131 | def resolve(self, doc, default=_nothing): | |
|
132 | """Resolves the pointer against doc and returns the referenced object""" | |
|
133 | ||
|
134 | for part in self.parts: | |
|
135 | ||
|
136 | try: | |
|
137 | doc = self.walk(doc, part) | |
|
138 | except JsonPointerException: | |
|
139 | if default is _nothing: | |
|
140 | raise | |
|
141 | else: | |
|
142 | return default | |
|
143 | ||
|
144 | return doc | |
|
145 | ||
|
146 | ||
|
147 | get = resolve | |
|
148 | ||
|
149 | ||
|
150 | def set(self, doc, value, path=None): | |
|
151 | """ Sets a field of doc to value | |
|
152 | ||
|
153 | The location of the field is given by the pointers base location and | |
|
154 | the optional path which is relative to the base location """ | |
|
155 | ||
|
156 | fullpath = list(self.parts) | |
|
157 | ||
|
158 | if path: | |
|
159 | fullpath += path.split('/') | |
|
160 | ||
|
161 | ||
|
162 | for part, nextpart in pairwise(fullpath): | |
|
163 | try: | |
|
164 | doc = self.walk(doc, part) | |
|
165 | except JsonPointerException: | |
|
166 | step_val = [] if nextpart.isdigit() else {} | |
|
167 | doc = self._set_value(doc, part, step_val) | |
|
168 | ||
|
169 | self._set_value(doc, fullpath[-1], value) | |
|
170 | ||
|
171 | ||
|
172 | @staticmethod | |
|
173 | def _set_value(doc, part, value): | |
|
174 | part = int(part) if part.isdigit() else part | |
|
175 | ||
|
176 | if isinstance(doc, dict): | |
|
177 | doc[part] = value | |
|
178 | ||
|
179 | if isinstance(doc, list): | |
|
180 | if len(doc) < part: | |
|
181 | doc[part] = value | |
|
182 | ||
|
183 | if len(doc) == part: | |
|
184 | doc.append(value) | |
|
185 | ||
|
186 | else: | |
|
187 | raise IndexError | |
|
188 | ||
|
189 | return doc[part] | |
|
190 | ||
|
191 | ||
|
192 | def walk(self, doc, part): | |
|
193 | """ Walks one step in doc and returns the referenced part """ | |
|
194 | ||
|
195 | # Its not clear if a location "1" should be considered as 1 or "1" | |
|
196 | # We prefer the integer-variant if possible | |
|
197 | part_variants = self._try_parse(part) + [part] | |
|
198 | ||
|
199 | for variant in part_variants: | |
|
200 | try: | |
|
201 | return doc[variant] | |
|
202 | except: | |
|
203 | continue | |
|
204 | ||
|
205 | raise JsonPointerException("'%s' not found in %s" % (part, doc)) | |
|
206 | ||
|
207 | ||
|
208 | @staticmethod | |
|
209 | def _try_parse(val, cls=int): | |
|
210 | try: | |
|
211 | return [cls(val)] | |
|
212 | except: | |
|
213 | return [] | |
|
214 | ||
|
215 | ||
|
216 | ||
|
217 | def pairwise(iterable): | |
|
218 | "s -> (s0,s1), (s1,s2), (s2, s3), ..." | |
|
219 | a, b = tee(iterable) | |
|
220 | next(b, None) | |
|
221 | return izip(a, b) | |
|
222 | __author__ = 'Stefan Kögl <stefan@skoegl.net>' |
This diff has been collapsed as it changes many lines, (653 lines changed) Show them Hide them | |||
@@ -0,0 +1,653 | |||
|
1 | """ | |
|
2 | An implementation of JSON Schema for Python | |
|
3 | ||
|
4 | The main functionality is provided by the :class:`Validator` class, with the | |
|
5 | :function:`validate` function being the most common way to quickly create a | |
|
6 | :class:`Validator` object and validate an instance with a given schema. | |
|
7 | ||
|
8 | The :class:`Validator` class generally attempts to be as strict as possible | |
|
9 | under the JSON Schema specification. See its docstring for details. | |
|
10 | ||
|
11 | """ | |
|
12 | ||
|
13 | from __future__ import division, unicode_literals | |
|
14 | ||
|
15 | import collections | |
|
16 | import itertools | |
|
17 | import operator | |
|
18 | import re | |
|
19 | import sys | |
|
20 | import warnings | |
|
21 | ||
|
22 | ||
|
23 | PY3 = sys.version_info[0] >= 3 | |
|
24 | ||
|
25 | if PY3: | |
|
26 | basestring = unicode = str | |
|
27 | iteritems = operator.methodcaller("items") | |
|
28 | else: | |
|
29 | from itertools import izip as zip | |
|
30 | iteritems = operator.methodcaller("iteritems") | |
|
31 | ||
|
32 | ||
|
33 | def _uniq(container): | |
|
34 | """ | |
|
35 | Check if all of a container's elements are unique. | |
|
36 | ||
|
37 | Successively tries first to rely that the elements are hashable, then | |
|
38 | falls back on them being sortable, and finally falls back on brute | |
|
39 | force. | |
|
40 | ||
|
41 | """ | |
|
42 | ||
|
43 | try: | |
|
44 | return len(set(container)) == len(container) | |
|
45 | except TypeError: | |
|
46 | try: | |
|
47 | sort = sorted(container) | |
|
48 | sliced = itertools.islice(container, 1, None) | |
|
49 | for i, j in zip(container, sliced): | |
|
50 | if i == j: | |
|
51 | return False | |
|
52 | except (NotImplementedError, TypeError): | |
|
53 | seen = [] | |
|
54 | for e in container: | |
|
55 | if e in seen: | |
|
56 | return False | |
|
57 | seen.append(e) | |
|
58 | return True | |
|
59 | ||
|
60 | ||
|
61 | __version__ = "0.5" | |
|
62 | ||
|
63 | ||
|
64 | DRAFT_3 = { | |
|
65 | "$schema" : "http://json-schema.org/draft-03/schema#", | |
|
66 | "id" : "http://json-schema.org/draft-03/schema#", | |
|
67 | "type" : "object", | |
|
68 | ||
|
69 | "properties" : { | |
|
70 | "type" : { | |
|
71 | "type" : ["string", "array"], | |
|
72 | "items" : {"type" : ["string", {"$ref" : "#"}]}, | |
|
73 | "uniqueItems" : True, | |
|
74 | "default" : "any" | |
|
75 | }, | |
|
76 | "properties" : { | |
|
77 | "type" : "object", | |
|
78 | "additionalProperties" : {"$ref" : "#", "type": "object"}, | |
|
79 | "default" : {} | |
|
80 | }, | |
|
81 | "patternProperties" : { | |
|
82 | "type" : "object", | |
|
83 | "additionalProperties" : {"$ref" : "#"}, | |
|
84 | "default" : {} | |
|
85 | }, | |
|
86 | "additionalProperties" : { | |
|
87 | "type" : [{"$ref" : "#"}, "boolean"], "default" : {} | |
|
88 | }, | |
|
89 | "items" : { | |
|
90 | "type" : [{"$ref" : "#"}, "array"], | |
|
91 | "items" : {"$ref" : "#"}, | |
|
92 | "default" : {} | |
|
93 | }, | |
|
94 | "additionalItems" : { | |
|
95 | "type" : [{"$ref" : "#"}, "boolean"], "default" : {} | |
|
96 | }, | |
|
97 | "required" : {"type" : "boolean", "default" : False}, | |
|
98 | "dependencies" : { | |
|
99 | "type" : ["string", "array", "object"], | |
|
100 | "additionalProperties" : { | |
|
101 | "type" : ["string", "array", {"$ref" : "#"}], | |
|
102 | "items" : {"type" : "string"} | |
|
103 | }, | |
|
104 | "default" : {} | |
|
105 | }, | |
|
106 | "minimum" : {"type" : "number"}, | |
|
107 | "maximum" : {"type" : "number"}, | |
|
108 | "exclusiveMinimum" : {"type" : "boolean", "default" : False}, | |
|
109 | "exclusiveMaximum" : {"type" : "boolean", "default" : False}, | |
|
110 | "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0}, | |
|
111 | "maxItems" : {"type" : "integer", "minimum" : 0}, | |
|
112 | "uniqueItems" : {"type" : "boolean", "default" : False}, | |
|
113 | "pattern" : {"type" : "string", "format" : "regex"}, | |
|
114 | "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0}, | |
|
115 | "maxLength" : {"type" : "integer"}, | |
|
116 | "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True}, | |
|
117 | "default" : {"type" : "any"}, | |
|
118 | "title" : {"type" : "string"}, | |
|
119 | "description" : {"type" : "string"}, | |
|
120 | "format" : {"type" : "string"}, | |
|
121 | "maxDecimal" : {"type" : "number", "minimum" : 0}, | |
|
122 | "divisibleBy" : { | |
|
123 | "type" : "number", | |
|
124 | "minimum" : 0, | |
|
125 | "exclusiveMinimum" : True, | |
|
126 | "default" : 1 | |
|
127 | }, | |
|
128 | "disallow" : { | |
|
129 | "type" : ["string", "array"], | |
|
130 | "items" : {"type" : ["string", {"$ref" : "#"}]}, | |
|
131 | "uniqueItems" : True | |
|
132 | }, | |
|
133 | "extends" : { | |
|
134 | "type" : [{"$ref" : "#"}, "array"], | |
|
135 | "items" : {"$ref" : "#"}, | |
|
136 | "default" : {} | |
|
137 | }, | |
|
138 | "id" : {"type" : "string", "format" : "uri"}, | |
|
139 | "$ref" : {"type" : "string", "format" : "uri"}, | |
|
140 | "$schema" : {"type" : "string", "format" : "uri"}, | |
|
141 | }, | |
|
142 | "dependencies" : { | |
|
143 | "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum" | |
|
144 | }, | |
|
145 | } | |
|
146 | ||
|
147 | EPSILON = 10 ** -15 | |
|
148 | ||
|
149 | ||
|
150 | class SchemaError(Exception): | |
|
151 | """ | |
|
152 | The provided schema is malformed. | |
|
153 | ||
|
154 | The same attributes exist for ``SchemaError``s as for ``ValidationError``s. | |
|
155 | ||
|
156 | """ | |
|
157 | ||
|
158 | validator = None | |
|
159 | ||
|
160 | def __init__(self, message): | |
|
161 | super(SchemaError, self).__init__(message) | |
|
162 | self.message = message | |
|
163 | self.path = [] | |
|
164 | ||
|
165 | ||
|
166 | class ValidationError(Exception): | |
|
167 | """ | |
|
168 | The instance didn't properly validate with the provided schema. | |
|
169 | ||
|
170 | Relevant attributes are: | |
|
171 | * ``message`` : a human readable message explaining the error | |
|
172 | * ``path`` : a list containing the path to the offending element (or [] | |
|
173 | if the error happened globally) in *reverse* order (i.e. | |
|
174 | deepest index first). | |
|
175 | ||
|
176 | """ | |
|
177 | ||
|
178 | # the failing validator will be set externally at whatever recursion level | |
|
179 | # is immediately above the validation failure | |
|
180 | validator = None | |
|
181 | ||
|
182 | def __init__(self, message): | |
|
183 | super(ValidationError, self).__init__(message) | |
|
184 | self.message = message | |
|
185 | ||
|
186 | # Any validator that recurses must append to the ValidationError's | |
|
187 | # path (e.g., properties and items) | |
|
188 | self.path = [] | |
|
189 | ||
|
190 | ||
|
191 | class Validator(object): | |
|
192 | """ | |
|
193 | A JSON Schema validator. | |
|
194 | ||
|
195 | """ | |
|
196 | ||
|
197 | DEFAULT_TYPES = { | |
|
198 | "array" : list, "boolean" : bool, "integer" : int, "null" : type(None), | |
|
199 | "number" : (int, float), "object" : dict, "string" : basestring, | |
|
200 | } | |
|
201 | ||
|
202 | def __init__( | |
|
203 | self, version=DRAFT_3, unknown_type="skip", | |
|
204 | unknown_property="skip", types=(), | |
|
205 | ): | |
|
206 | """ | |
|
207 | Initialize a Validator. | |
|
208 | ||
|
209 | ``version`` specifies which version of the JSON Schema specification to | |
|
210 | validate with. Currently only draft-03 is supported (and is the | |
|
211 | default). | |
|
212 | ||
|
213 | ``unknown_type`` and ``unknown_property`` control what to do when an | |
|
214 | unknown type (resp. property) is encountered. By default, the | |
|
215 | metaschema is respected (which e.g. for draft 3 allows a schema to have | |
|
216 | additional properties), but if for some reason you want to modify this | |
|
217 | behavior, you can do so without needing to modify the metaschema by | |
|
218 | passing ``"error"`` or ``"warn"`` to these arguments. | |
|
219 | ||
|
220 | ``types`` is a mapping (or iterable of 2-tuples) containing additional | |
|
221 | types or alternate types to verify via the 'type' property. For | |
|
222 | instance, the default types for the 'number' JSON Schema type are | |
|
223 | ``int`` and ``float``. To override this behavior (e.g. for also | |
|
224 | allowing ``decimal.Decimal``), pass ``types={"number" : (int, float, | |
|
225 | decimal.Decimal)} *including* the default types if so desired, which | |
|
226 | are fairly obvious but can be accessed via ``Validator.DEFAULT_TYPES`` | |
|
227 | if necessary. | |
|
228 | ||
|
229 | """ | |
|
230 | ||
|
231 | self._unknown_type = unknown_type | |
|
232 | self._unknown_property = unknown_property | |
|
233 | self._version = version | |
|
234 | ||
|
235 | self._types = dict(self.DEFAULT_TYPES) | |
|
236 | self._types.update(types) | |
|
237 | self._types["any"] = tuple(self._types.values()) | |
|
238 | ||
|
239 | def is_type(self, instance, type): | |
|
240 | """ | |
|
241 | Check if an ``instance`` is of the provided ``type``. | |
|
242 | ||
|
243 | """ | |
|
244 | ||
|
245 | py_type = self._types.get(type) | |
|
246 | ||
|
247 | if py_type is None: | |
|
248 | return self.schema_error( | |
|
249 | self._unknown_type, "%r is not a known type" % (type,) | |
|
250 | ) | |
|
251 | ||
|
252 | # the only thing we're careful about here is evading bool inheriting | |
|
253 | # from int, so let's be even dirtier than usual | |
|
254 | ||
|
255 | elif ( | |
|
256 | # it's not a bool, so no worries | |
|
257 | not isinstance(instance, bool) or | |
|
258 | ||
|
259 | # it is a bool, but we're checking for a bool, so no worries | |
|
260 | ( | |
|
261 | py_type is bool or | |
|
262 | isinstance(py_type, tuple) and bool in py_type | |
|
263 | ) | |
|
264 | ||
|
265 | ): | |
|
266 | return isinstance(instance, py_type) | |
|
267 | ||
|
268 | def schema_error(self, level, msg): | |
|
269 | if level == "skip": | |
|
270 | return | |
|
271 | elif level == "warn": | |
|
272 | warnings.warn(msg) | |
|
273 | else: | |
|
274 | raise SchemaError(msg) | |
|
275 | ||
|
276 | def is_valid(self, instance, schema, meta_validate=True): | |
|
277 | """ | |
|
278 | Check if the ``instance`` is valid under the ``schema``. | |
|
279 | ||
|
280 | Returns a bool indicating whether validation succeeded. | |
|
281 | ||
|
282 | """ | |
|
283 | ||
|
284 | error = next(self.iter_errors(instance, schema, meta_validate), None) | |
|
285 | return error is None | |
|
286 | ||
|
287 | def iter_errors(self, instance, schema, meta_validate=True): | |
|
288 | """ | |
|
289 | Lazily yield each of the errors in the given ``instance``. | |
|
290 | ||
|
291 | If you are unsure whether your schema itself is valid, | |
|
292 | ``meta_validate`` will first validate that the schema is valid before | |
|
293 | attempting to validate the instance. ``meta_validate`` is ``True`` by | |
|
294 | default, since setting it to ``False`` can lead to confusing error | |
|
295 | messages with an invalid schema. If you're sure your schema is in fact | |
|
296 | valid, or don't care, feel free to set this to ``False``. The meta | |
|
297 | validation will be done using the appropriate ``version``. | |
|
298 | ||
|
299 | """ | |
|
300 | ||
|
301 | if meta_validate: | |
|
302 | for error in self.iter_errors( | |
|
303 | schema, self._version, meta_validate=False | |
|
304 | ): | |
|
305 | s = SchemaError(error.message) | |
|
306 | s.path = error.path | |
|
307 | s.validator = error.validator | |
|
308 | # I think we're safer raising these always, not yielding them | |
|
309 | raise s | |
|
310 | ||
|
311 | for k, v in iteritems(schema): | |
|
312 | validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) | |
|
313 | ||
|
314 | if validator is None: | |
|
315 | errors = self.unknown_property(k, instance, schema) | |
|
316 | else: | |
|
317 | errors = validator(v, instance, schema) | |
|
318 | ||
|
319 | for error in errors or (): | |
|
320 | # if the validator hasn't already been set (due to recursion) | |
|
321 | # make sure to set it | |
|
322 | error.validator = error.validator or k | |
|
323 | yield error | |
|
324 | ||
|
325 | def validate(self, *args, **kwargs): | |
|
326 | """ | |
|
327 | Validate an ``instance`` under the given ``schema``. | |
|
328 | ||
|
329 | """ | |
|
330 | ||
|
331 | for error in self.iter_errors(*args, **kwargs): | |
|
332 | raise error | |
|
333 | ||
|
334 | def unknown_property(self, property, instance, schema): | |
|
335 | self.schema_error( | |
|
336 | self._unknown_property, | |
|
337 | "%r is not a known schema property" % (property,) | |
|
338 | ) | |
|
339 | ||
|
340 | def validate_type(self, types, instance, schema): | |
|
341 | types = _list(types) | |
|
342 | ||
|
343 | for type in types: | |
|
344 | # Ouch. Brain hurts. Two paths here, either we have a schema, then | |
|
345 | # check if the instance is valid under it | |
|
346 | if (( | |
|
347 | self.is_type(type, "object") and | |
|
348 | self.is_valid(instance, type) | |
|
349 | ||
|
350 | # Or we have a type as a string, just check if the instance is that | |
|
351 | # type. Also, HACK: we can reach the `or` here if skip_types is | |
|
352 | # something other than error. If so, bail out. | |
|
353 | ||
|
354 | ) or ( | |
|
355 | self.is_type(type, "string") and | |
|
356 | (self.is_type(instance, type) or type not in self._types) | |
|
357 | )): | |
|
358 | return | |
|
359 | else: | |
|
360 | yield ValidationError( | |
|
361 | "%r is not of type %r" % (instance, _delist(types)) | |
|
362 | ) | |
|
363 | ||
|
364 | def validate_properties(self, properties, instance, schema): | |
|
365 | if not self.is_type(instance, "object"): | |
|
366 | return | |
|
367 | ||
|
368 | for property, subschema in iteritems(properties): | |
|
369 | if property in instance: | |
|
370 | dependencies = _list(subschema.get("dependencies", [])) | |
|
371 | if self.is_type(dependencies, "object"): | |
|
372 | for error in self.iter_errors( | |
|
373 | instance, dependencies, meta_validate=False | |
|
374 | ): | |
|
375 | yield error | |
|
376 | else: | |
|
377 | for dependency in dependencies: | |
|
378 | if dependency not in instance: | |
|
379 | yield ValidationError( | |
|
380 | "%r is a dependency of %r" % (dependency, property) | |
|
381 | ) | |
|
382 | ||
|
383 | for error in self.iter_errors( | |
|
384 | instance[property], subschema, meta_validate=False | |
|
385 | ): | |
|
386 | error.path.append(property) | |
|
387 | yield error | |
|
388 | elif subschema.get("required", False): | |
|
389 | error = ValidationError( | |
|
390 | "%r is a required property" % (property,) | |
|
391 | ) | |
|
392 | error.path.append(property) | |
|
393 | error.validator = "required" | |
|
394 | yield error | |
|
395 | ||
|
396 | def validate_patternProperties(self, patternProperties, instance, schema): | |
|
397 | for pattern, subschema in iteritems(patternProperties): | |
|
398 | for k, v in iteritems(instance): | |
|
399 | if re.match(pattern, k): | |
|
400 | for error in self.iter_errors( | |
|
401 | v, subschema, meta_validate=False | |
|
402 | ): | |
|
403 | yield error | |
|
404 | ||
|
405 | def validate_additionalProperties(self, aP, instance, schema): | |
|
406 | if not self.is_type(instance, "object"): | |
|
407 | return | |
|
408 | ||
|
409 | # no viewkeys in <2.7, and pypy seems to fail on vk - vk anyhow, so... | |
|
410 | extras = set(instance) - set(schema.get("properties", {})) | |
|
411 | ||
|
412 | if self.is_type(aP, "object"): | |
|
413 | for extra in extras: | |
|
414 | for error in self.iter_errors( | |
|
415 | instance[extra], aP, meta_validate=False | |
|
416 | ): | |
|
417 | yield error | |
|
418 | elif not aP and extras: | |
|
419 | error = "Additional properties are not allowed (%s %s unexpected)" | |
|
420 | yield ValidationError(error % _extras_msg(extras)) | |
|
421 | ||
|
422 | def validate_items(self, items, instance, schema): | |
|
423 | if not self.is_type(instance, "array"): | |
|
424 | return | |
|
425 | ||
|
426 | if self.is_type(items, "object"): | |
|
427 | for index, item in enumerate(instance): | |
|
428 | for error in self.iter_errors( | |
|
429 | item, items, meta_validate=False | |
|
430 | ): | |
|
431 | error.path.append(index) | |
|
432 | yield error | |
|
433 | else: | |
|
434 | for (index, item), subschema in zip(enumerate(instance), items): | |
|
435 | for error in self.iter_errors( | |
|
436 | item, subschema, meta_validate=False | |
|
437 | ): | |
|
438 | error.path.append(index) | |
|
439 | yield error | |
|
440 | ||
|
441 | def validate_additionalItems(self, aI, instance, schema): | |
|
442 | if not self.is_type(instance, "array"): | |
|
443 | return | |
|
444 | ||
|
445 | if self.is_type(aI, "object"): | |
|
446 | for item in instance[len(schema):]: | |
|
447 | for error in self.iter_errors(item, aI, meta_validate=False): | |
|
448 | yield error | |
|
449 | elif not aI and len(instance) > len(schema.get("items", [])): | |
|
450 | error = "Additional items are not allowed (%s %s unexpected)" | |
|
451 | yield ValidationError( | |
|
452 | error % _extras_msg(instance[len(schema) - 1:]) | |
|
453 | ) | |
|
454 | ||
|
455 | def validate_minimum(self, minimum, instance, schema): | |
|
456 | if not self.is_type(instance, "number"): | |
|
457 | return | |
|
458 | ||
|
459 | instance = float(instance) | |
|
460 | if schema.get("exclusiveMinimum", False): | |
|
461 | failed = instance <= minimum | |
|
462 | cmp = "less than or equal to" | |
|
463 | else: | |
|
464 | failed = instance < minimum | |
|
465 | cmp = "less than" | |
|
466 | ||
|
467 | if failed: | |
|
468 | yield ValidationError( | |
|
469 | "%r is %s the minimum of %r" % (instance, cmp, minimum) | |
|
470 | ) | |
|
471 | ||
|
472 | def validate_maximum(self, maximum, instance, schema): | |
|
473 | if not self.is_type(instance, "number"): | |
|
474 | return | |
|
475 | ||
|
476 | instance = float(instance) | |
|
477 | if schema.get("exclusiveMaximum", False): | |
|
478 | failed = instance >= maximum | |
|
479 | cmp = "greater than or equal to" | |
|
480 | else: | |
|
481 | failed = instance > maximum | |
|
482 | cmp = "greater than" | |
|
483 | ||
|
484 | if failed: | |
|
485 | yield ValidationError( | |
|
486 | "%r is %s the maximum of %r" % (instance, cmp, maximum) | |
|
487 | ) | |
|
488 | ||
|
489 | def validate_minItems(self, mI, instance, schema): | |
|
490 | if self.is_type(instance, "array") and len(instance) < mI: | |
|
491 | yield ValidationError("%r is too short" % (instance,)) | |
|
492 | ||
|
493 | def validate_maxItems(self, mI, instance, schema): | |
|
494 | if self.is_type(instance, "array") and len(instance) > mI: | |
|
495 | yield ValidationError("%r is too long" % (instance,)) | |
|
496 | ||
|
497 | def validate_uniqueItems(self, uI, instance, schema): | |
|
498 | if uI and self.is_type(instance, "array") and not _uniq(instance): | |
|
499 | yield ValidationError("%r has non-unique elements" % instance) | |
|
500 | ||
|
501 | def validate_pattern(self, patrn, instance, schema): | |
|
502 | if self.is_type(instance, "string") and not re.match(patrn, instance): | |
|
503 | yield ValidationError("%r does not match %r" % (instance, patrn)) | |
|
504 | ||
|
505 | def validate_minLength(self, mL, instance, schema): | |
|
506 | if self.is_type(instance, "string") and len(instance) < mL: | |
|
507 | yield ValidationError("%r is too short" % (instance,)) | |
|
508 | ||
|
509 | def validate_maxLength(self, mL, instance, schema): | |
|
510 | if self.is_type(instance, "string") and len(instance) > mL: | |
|
511 | yield ValidationError("%r is too long" % (instance,)) | |
|
512 | ||
|
513 | def validate_enum(self, enums, instance, schema): | |
|
514 | if instance not in enums: | |
|
515 | yield ValidationError("%r is not one of %r" % (instance, enums)) | |
|
516 | ||
|
517 | def validate_divisibleBy(self, dB, instance, schema): | |
|
518 | if not self.is_type(instance, "number"): | |
|
519 | return | |
|
520 | ||
|
521 | if isinstance(dB, float): | |
|
522 | mod = instance % dB | |
|
523 | failed = (mod > EPSILON) and (dB - mod) > EPSILON | |
|
524 | else: | |
|
525 | failed = instance % dB | |
|
526 | ||
|
527 | if failed: | |
|
528 | yield ValidationError("%r is not divisible by %r" % (instance, dB)) | |
|
529 | ||
|
530 | def validate_disallow(self, disallow, instance, schema): | |
|
531 | for disallowed in _list(disallow): | |
|
532 | if self.is_valid(instance, {"type" : [disallowed]}): | |
|
533 | yield ValidationError( | |
|
534 | "%r is disallowed for %r" % (disallowed, instance) | |
|
535 | ) | |
|
536 | ||
|
537 | def validate_extends(self, extends, instance, schema): | |
|
538 | if self.is_type(extends, "object"): | |
|
539 | extends = [extends] | |
|
540 | for subschema in extends: | |
|
541 | for error in self.iter_errors( | |
|
542 | instance, subschema, meta_validate=False | |
|
543 | ): | |
|
544 | yield error | |
|
545 | ||
|
546 | ||
|
547 | for no_op in [ # handled in: | |
|
548 | "dependencies", "required", # properties | |
|
549 | "exclusiveMinimum", "exclusiveMaximum", # min*/max* | |
|
550 | "default", "description", "format", "id", # no validation needed | |
|
551 | "links", "name", "title", | |
|
552 | "ref", "schema", # not yet supported | |
|
553 | ]: | |
|
554 | setattr(Validator, "validate_" + no_op, lambda *args, **kwargs : None) | |
|
555 | ||
|
556 | ||
|
557 | class ErrorTree(object): | |
|
558 | """ | |
|
559 | ErrorTrees make it easier to check which validations failed. | |
|
560 | ||
|
561 | """ | |
|
562 | ||
|
563 | def __init__(self, errors=()): | |
|
564 | self.errors = {} | |
|
565 | self._contents = collections.defaultdict(self.__class__) | |
|
566 | ||
|
567 | for error in errors: | |
|
568 | container = self | |
|
569 | for element in reversed(error.path): | |
|
570 | container = container[element] | |
|
571 | container.errors[error.validator] = error | |
|
572 | ||
|
573 | def __contains__(self, k): | |
|
574 | return k in self._contents | |
|
575 | ||
|
576 | def __getitem__(self, k): | |
|
577 | return self._contents[k] | |
|
578 | ||
|
579 | def __setitem__(self, k, v): | |
|
580 | self._contents[k] = v | |
|
581 | ||
|
582 | def __iter__(self): | |
|
583 | return iter(self._contents) | |
|
584 | ||
|
585 | def __len__(self): | |
|
586 | child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) | |
|
587 | return len(self.errors) + child_errors | |
|
588 | ||
|
589 | def __repr__(self): | |
|
590 | return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) | |
|
591 | ||
|
592 | ||
|
593 | def _extras_msg(extras): | |
|
594 | """ | |
|
595 | Create an error message for extra items or properties. | |
|
596 | ||
|
597 | """ | |
|
598 | ||
|
599 | if len(extras) == 1: | |
|
600 | verb = "was" | |
|
601 | else: | |
|
602 | verb = "were" | |
|
603 | return ", ".join(repr(extra) for extra in extras), verb | |
|
604 | ||
|
605 | ||
|
606 | def _list(thing): | |
|
607 | """ | |
|
608 | Wrap ``thing`` in a list if it's a single str. | |
|
609 | ||
|
610 | Otherwise, return it unchanged. | |
|
611 | ||
|
612 | """ | |
|
613 | ||
|
614 | if isinstance(thing, basestring): | |
|
615 | return [thing] | |
|
616 | return thing | |
|
617 | ||
|
618 | ||
|
619 | def _delist(thing): | |
|
620 | """ | |
|
621 | Unwrap ``thing`` to a single element if its a single str in a list. | |
|
622 | ||
|
623 | Otherwise, return it unchanged. | |
|
624 | ||
|
625 | """ | |
|
626 | ||
|
627 | if ( | |
|
628 | isinstance(thing, list) and | |
|
629 | len(thing) == 1 | |
|
630 | and isinstance(thing[0], basestring) | |
|
631 | ): | |
|
632 | return thing[0] | |
|
633 | return thing | |
|
634 | ||
|
635 | ||
|
636 | def validate( | |
|
637 | instance, schema, meta_validate=True, cls=Validator, *args, **kwargs | |
|
638 | ): | |
|
639 | """ | |
|
640 | Validate an ``instance`` under the given ``schema``. | |
|
641 | ||
|
642 | By default, the :class:`Validator` class from this module is used to | |
|
643 | perform the validation. To use another validator, pass it into the ``cls`` | |
|
644 | argument. | |
|
645 | ||
|
646 | Any other provided positional and keyword arguments will be provided to the | |
|
647 | ``cls``. See the :class:`Validator` class' docstring for details on the | |
|
648 | arguments it accepts. | |
|
649 | ||
|
650 | """ | |
|
651 | ||
|
652 | validator = cls(*args, **kwargs) | |
|
653 | validator.validate(instance, schema, meta_validate=meta_validate) |
@@ -158,7 +158,7 | |||
|
158 | 158 | }, |
|
159 | 159 | "prompt_number": { |
|
160 | 160 | "type": ["integer","null"], |
|
161 | "required": true | |
|
161 | "required": true, | |
|
162 | 162 | "minimum": 0 |
|
163 | 163 | }, |
|
164 | 164 | "language": { |
@@ -1,8 +1,8 | |||
|
1 | 1 | #!/usr/bin/env python |
|
2 | 2 | # -*- coding: utf8 -*- |
|
3 | 3 | |
|
4 | from jsonschema import Validator, validate, ValidationError | |
|
5 | import jsonpointer | |
|
4 | from IPython.external.jsonschema import Validator, validate, ValidationError | |
|
5 | import IPython.external.jsonpointer as jsonpointer | |
|
6 | 6 | import argparse |
|
7 | 7 | import traceback |
|
8 | 8 | import json |
General Comments 0
You need to be logged in to leave comments.
Login now