Show More
This diff has been collapsed as it changes many lines, (587 lines changed) Show them Hide them | |||||
@@ -1,653 +1,734 | |||||
1 | """ |
|
1 | """ | |
2 | An implementation of JSON Schema for Python |
|
2 | An implementation of JSON Schema for Python | |
3 |
|
3 | |||
4 |
The main functionality is provided by the |
|
4 | The main functionality is provided by the validator classes for each of the | |
5 | :function:`validate` function being the most common way to quickly create a |
|
5 | supported JSON Schema versions. | |
6 | :class:`Validator` object and validate an instance with a given schema. |
|
|||
7 |
|
6 | |||
8 | The :class:`Validator` class generally attempts to be as strict as possible |
|
7 | Most commonly, the :function:`validate` function is the quickest way to simply | |
9 | under the JSON Schema specification. See its docstring for details. |
|
8 | validate a given instance under a schema, and will create a validator for you. | |
10 |
|
9 | |||
11 | """ |
|
10 | """ | |
12 |
|
11 | |||
13 | from __future__ import division, unicode_literals |
|
12 | from __future__ import division, unicode_literals | |
14 |
|
13 | |||
15 | import collections |
|
14 | import collections | |
16 | import itertools |
|
15 | import itertools | |
17 | import operator |
|
16 | import operator | |
18 | import re |
|
17 | import re | |
19 | import sys |
|
18 | import sys | |
20 | import warnings |
|
19 | import warnings | |
21 |
|
20 | |||
22 |
|
21 | |||
|
22 | __version__ = "0.7" | |||
|
23 | ||||
|
24 | FLOAT_TOLERANCE = 10 ** -15 | |||
23 | PY3 = sys.version_info[0] >= 3 |
|
25 | PY3 = sys.version_info[0] >= 3 | |
24 |
|
26 | |||
25 | if PY3: |
|
27 | if PY3: | |
26 | basestring = unicode = str |
|
28 | basestring = unicode = str | |
27 | iteritems = operator.methodcaller("items") |
|
29 | iteritems = operator.methodcaller("items") | |
|
30 | from urllib.parse import unquote | |||
28 | else: |
|
31 | else: | |
29 | from itertools import izip as zip |
|
32 | from itertools import izip as zip | |
30 | iteritems = operator.methodcaller("iteritems") |
|
33 | iteritems = operator.methodcaller("iteritems") | |
|
34 | from urllib import unquote | |||
31 |
|
35 | |||
32 |
|
36 | |||
33 | def _uniq(container): |
|
37 | class UnknownType(Exception): | |
34 | """ |
|
38 | """ | |
35 | Check if all of a container's elements are unique. |
|
39 | An unknown type was given. | |
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 |
|
40 | |||
41 | """ |
|
41 | """ | |
42 |
|
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 |
|
43 | |||
61 | __version__ = "0.5" |
|
44 | class InvalidRef(Exception): | |
62 |
|
45 | """ | ||
63 |
|
46 | An invalid reference was given. | ||
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 |
|
|
47 | ||
147 | EPSILON = 10 ** -15 |
|
48 | """ | |
148 |
|
49 | |||
149 |
|
50 | |||
150 | class SchemaError(Exception): |
|
51 | class SchemaError(Exception): | |
151 | """ |
|
52 | """ | |
152 | The provided schema is malformed. |
|
53 | The provided schema is malformed. | |
153 |
|
54 | |||
154 | The same attributes exist for ``SchemaError``s as for ``ValidationError``s. |
|
55 | The same attributes exist for ``SchemaError``s as for ``ValidationError``s. | |
155 |
|
56 | |||
156 | """ |
|
57 | """ | |
157 |
|
58 | |||
158 | validator = None |
|
59 | validator = None | |
159 |
|
60 | |||
160 | def __init__(self, message): |
|
61 | def __init__(self, message): | |
161 | super(SchemaError, self).__init__(message) |
|
62 | super(SchemaError, self).__init__(message) | |
162 | self.message = message |
|
63 | self.message = message | |
163 | self.path = [] |
|
64 | self.path = [] | |
164 |
|
65 | |||
165 |
|
66 | |||
166 | class ValidationError(Exception): |
|
67 | class ValidationError(Exception): | |
167 | """ |
|
68 | """ | |
168 | The instance didn't properly validate with the provided schema. |
|
69 | The instance didn't properly validate with the provided schema. | |
169 |
|
70 | |||
170 | Relevant attributes are: |
|
71 | Relevant attributes are: | |
171 | * ``message`` : a human readable message explaining the error |
|
72 | * ``message`` : a human readable message explaining the error | |
172 | * ``path`` : a list containing the path to the offending element (or [] |
|
73 | * ``path`` : a list containing the path to the offending element (or [] | |
173 | if the error happened globally) in *reverse* order (i.e. |
|
74 | if the error happened globally) in *reverse* order (i.e. | |
174 | deepest index first). |
|
75 | deepest index first). | |
175 |
|
76 | |||
176 | """ |
|
77 | """ | |
177 |
|
78 | |||
178 | # the failing validator will be set externally at whatever recursion level |
|
79 | # the failing validator will be set externally at whatever recursion level | |
179 | # is immediately above the validation failure |
|
80 | # is immediately above the validation failure | |
180 | validator = None |
|
81 | validator = None | |
181 |
|
82 | |||
182 | def __init__(self, message): |
|
83 | def __init__(self, message): | |
183 | super(ValidationError, self).__init__(message) |
|
84 | super(ValidationError, self).__init__(message) | |
184 | self.message = message |
|
85 | self.message = message | |
185 |
|
86 | |||
186 | # Any validator that recurses must append to the ValidationError's |
|
87 | # Any validator that recurses must append to the ValidationError's | |
187 | # path (e.g., properties and items) |
|
88 | # path (e.g., properties and items) | |
188 | self.path = [] |
|
89 | self.path = [] | |
189 |
|
90 | |||
190 |
|
91 | |||
191 | class Validator(object): |
|
92 | class Draft3Validator(object): | |
192 | """ |
|
93 | """ | |
193 |
A JSON Schema |
|
94 | A validator for JSON Schema draft 3. | |
194 |
|
95 | |||
195 | """ |
|
96 | """ | |
196 |
|
97 | |||
197 | DEFAULT_TYPES = { |
|
98 | DEFAULT_TYPES = { | |
198 | "array" : list, "boolean" : bool, "integer" : int, "null" : type(None), |
|
99 | "array" : list, "boolean" : bool, "integer" : int, "null" : type(None), | |
199 | "number" : (int, float), "object" : dict, "string" : basestring, |
|
100 | "number" : (int, float), "object" : dict, "string" : basestring, | |
200 | } |
|
101 | } | |
201 |
|
102 | |||
202 | def __init__( |
|
103 | def __init__(self, schema, types=()): | |
203 | self, version=DRAFT_3, unknown_type="skip", |
|
|||
204 | unknown_property="skip", types=(), |
|
|||
205 | ): |
|
|||
206 | """ |
|
104 | """ | |
207 |
Initialize a |
|
105 | Initialize a validator. | |
208 |
|
106 | |||
209 | ``version`` specifies which version of the JSON Schema specification to |
|
107 | ``schema`` should be a *valid* JSON Schema object already converted to | |
210 | validate with. Currently only draft-03 is supported (and is the |
|
108 | a native Python object (typically a dict via ``json.load``). | |
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 |
|
109 | |||
220 | ``types`` is a mapping (or iterable of 2-tuples) containing additional |
|
110 | ``types`` is a mapping (or iterable of 2-tuples) containing additional | |
221 | types or alternate types to verify via the 'type' property. For |
|
111 | types or alternate types to verify via the 'type' property. For | |
222 | instance, the default types for the 'number' JSON Schema type are |
|
112 | instance, the default types for the 'number' JSON Schema type are | |
223 | ``int`` and ``float``. To override this behavior (e.g. for also |
|
113 | ``int`` and ``float``. To override this behavior (e.g. for also | |
224 | allowing ``decimal.Decimal``), pass ``types={"number" : (int, float, |
|
114 | allowing ``decimal.Decimal``), pass ``types={"number" : (int, float, | |
225 | decimal.Decimal)} *including* the default types if so desired, which |
|
115 | decimal.Decimal)} *including* the default types if so desired, which | |
226 |
are fairly obvious but can be accessed via `` |
|
116 | are fairly obvious but can be accessed via the ``DEFAULT_TYPES`` | |
227 | if necessary. |
|
117 | attribute on this class if necessary. | |
228 |
|
118 | |||
229 | """ |
|
119 | """ | |
230 |
|
120 | |||
231 | self._unknown_type = unknown_type |
|
|||
232 | self._unknown_property = unknown_property |
|
|||
233 | self._version = version |
|
|||
234 |
|
||||
235 | self._types = dict(self.DEFAULT_TYPES) |
|
121 | self._types = dict(self.DEFAULT_TYPES) | |
236 | self._types.update(types) |
|
122 | self._types.update(types) | |
237 | self._types["any"] = tuple(self._types.values()) |
|
123 | self._types["any"] = tuple(self._types.values()) | |
238 |
|
124 | |||
|
125 | self.schema = schema | |||
|
126 | ||||
239 | def is_type(self, instance, type): |
|
127 | def is_type(self, instance, type): | |
240 | """ |
|
128 | """ | |
241 | Check if an ``instance`` is of the provided ``type``. |
|
129 | Check if an ``instance`` is of the provided (JSON Schema) ``type``. | |
242 |
|
130 | |||
243 | """ |
|
131 | """ | |
244 |
|
132 | |||
245 |
|
|
133 | if type not in self._types: | |
246 |
|
134 | raise UnknownType(type) | ||
247 | if py_type is None: |
|
135 | type = self._types[type] | |
248 | return self.schema_error( |
|
|||
249 | self._unknown_type, "%r is not a known type" % (type,) |
|
|||
250 | ) |
|
|||
251 |
|
136 | |||
252 | # the only thing we're careful about here is evading bool inheriting |
|
137 | # bool inherits from int, so ensure bools aren't reported as integers | |
253 | # from int, so let's be even dirtier than usual |
|
138 | if isinstance(instance, bool): | |
254 |
|
139 | type = _flatten(type) | ||
255 | elif ( |
|
140 | if int in type and bool not in type: | |
256 | # it's not a bool, so no worries |
|
141 | return False | |
257 |
|
|
142 | return isinstance(instance, type) | |
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 |
|
143 | |||
276 |
def is_valid(self, instance, schema |
|
144 | def is_valid(self, instance, _schema=None): | |
277 | """ |
|
145 | """ | |
278 |
Check if the ``instance`` is valid under the |
|
146 | Check if the ``instance`` is valid under the current schema. | |
279 |
|
147 | |||
280 | Returns a bool indicating whether validation succeeded. |
|
148 | Returns a bool indicating whether validation succeeded. | |
281 |
|
149 | |||
282 | """ |
|
150 | """ | |
283 |
|
151 | |||
284 |
error = next(self.iter_errors(instance, schema |
|
152 | error = next(self.iter_errors(instance, _schema), None) | |
285 | return error is None |
|
153 | return error is None | |
286 |
|
154 | |||
287 | def iter_errors(self, instance, schema, meta_validate=True): |
|
155 | @classmethod | |
|
156 | def check_schema(cls, schema): | |||
288 | """ |
|
157 | """ | |
289 | Lazily yield each of the errors in the given ``instance``. |
|
158 | Validate a ``schema`` against the meta-schema to see if it is valid. | |
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 |
|
159 | |||
299 | """ |
|
160 | """ | |
300 |
|
161 | |||
301 | if meta_validate: |
|
162 | for error in cls(cls.META_SCHEMA).iter_errors(schema): | |
302 | for error in self.iter_errors( |
|
|||
303 | schema, self._version, meta_validate=False |
|
|||
304 | ): |
|
|||
305 |
|
|
163 | s = SchemaError(error.message) | |
306 |
|
|
164 | s.path = error.path | |
307 |
|
|
165 | s.validator = error.validator | |
308 |
|
|
166 | # I think we're safer raising these always, not yielding them | |
309 |
|
|
167 | raise s | |
310 |
|
168 | |||
311 | for k, v in iteritems(schema): |
|
169 | def iter_errors(self, instance, _schema=None): | |
|
170 | """ | |||
|
171 | Lazily yield each of the errors in the given ``instance``. | |||
|
172 | ||||
|
173 | """ | |||
|
174 | ||||
|
175 | if _schema is None: | |||
|
176 | _schema = self.schema | |||
|
177 | ||||
|
178 | for k, v in iteritems(_schema): | |||
312 | validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) |
|
179 | validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) | |
313 |
|
180 | |||
314 | if validator is None: |
|
181 | if validator is None: | |
315 | errors = self.unknown_property(k, instance, schema) |
|
182 | continue | |
316 | else: |
|
|||
317 | errors = validator(v, instance, schema) |
|
|||
318 |
|
183 | |||
319 | for error in errors or (): |
|
184 | errors = validator(v, instance, _schema) or () | |
|
185 | for error in errors: | |||
320 | # if the validator hasn't already been set (due to recursion) |
|
186 | # if the validator hasn't already been set (due to recursion) | |
321 | # make sure to set it |
|
187 | # make sure to set it | |
322 | error.validator = error.validator or k |
|
188 | error.validator = error.validator or k | |
323 | yield error |
|
189 | yield error | |
324 |
|
190 | |||
325 | def validate(self, *args, **kwargs): |
|
191 | def validate(self, *args, **kwargs): | |
326 | """ |
|
192 | """ | |
327 | Validate an ``instance`` under the given ``schema``. |
|
193 | Validate an ``instance`` under the given ``schema``. | |
328 |
|
194 | |||
329 | """ |
|
195 | """ | |
330 |
|
196 | |||
331 | for error in self.iter_errors(*args, **kwargs): |
|
197 | for error in self.iter_errors(*args, **kwargs): | |
332 | raise error |
|
198 | raise error | |
333 |
|
199 | |||
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): |
|
200 | def validate_type(self, types, instance, schema): | |
341 | types = _list(types) |
|
201 | types = _list(types) | |
342 |
|
202 | |||
343 | for type in types: |
|
203 | for type in types: | |
344 | # Ouch. Brain hurts. Two paths here, either we have a schema, then |
|
204 | # Ouch. Brain hurts. Two paths here, either we have a schema, then | |
345 | # check if the instance is valid under it |
|
205 | # check if the instance is valid under it | |
346 | if (( |
|
206 | if (( | |
347 | self.is_type(type, "object") and |
|
207 | self.is_type(type, "object") and | |
348 | self.is_valid(instance, type) |
|
208 | self.is_valid(instance, type) | |
349 |
|
209 | |||
350 | # Or we have a type as a string, just check if the instance is that |
|
210 | # 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 |
|
211 | # type. Also, HACK: we can reach the `or` here if skip_types is | |
352 | # something other than error. If so, bail out. |
|
212 | # something other than error. If so, bail out. | |
353 |
|
213 | |||
354 | ) or ( |
|
214 | ) or ( | |
355 | self.is_type(type, "string") and |
|
215 | self.is_type(type, "string") and | |
356 | (self.is_type(instance, type) or type not in self._types) |
|
216 | (self.is_type(instance, type) or type not in self._types) | |
357 | )): |
|
217 | )): | |
358 | return |
|
218 | return | |
359 | else: |
|
219 | else: | |
360 | yield ValidationError( |
|
220 | yield ValidationError(_types_msg(instance, types)) | |
361 | "%r is not of type %r" % (instance, _delist(types)) |
|
|||
362 | ) |
|
|||
363 |
|
221 | |||
364 | def validate_properties(self, properties, instance, schema): |
|
222 | def validate_properties(self, properties, instance, schema): | |
365 | if not self.is_type(instance, "object"): |
|
223 | if not self.is_type(instance, "object"): | |
366 | return |
|
224 | return | |
367 |
|
225 | |||
368 | for property, subschema in iteritems(properties): |
|
226 | for property, subschema in iteritems(properties): | |
369 | if property in instance: |
|
227 | if property in instance: | |
370 | dependencies = _list(subschema.get("dependencies", [])) |
|
228 | for error in self.iter_errors(instance[property], subschema): | |
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) |
|
229 | error.path.append(property) | |
387 | yield error |
|
230 | yield error | |
388 | elif subschema.get("required", False): |
|
231 | elif subschema.get("required", False): | |
389 | error = ValidationError( |
|
232 | error = ValidationError( | |
390 | "%r is a required property" % (property,) |
|
233 | "%r is a required property" % (property,) | |
391 | ) |
|
234 | ) | |
392 | error.path.append(property) |
|
235 | error.path.append(property) | |
393 | error.validator = "required" |
|
236 | error.validator = "required" | |
394 | yield error |
|
237 | yield error | |
395 |
|
238 | |||
396 | def validate_patternProperties(self, patternProperties, instance, schema): |
|
239 | def validate_patternProperties(self, patternProperties, instance, schema): | |
397 | for pattern, subschema in iteritems(patternProperties): |
|
240 | for pattern, subschema in iteritems(patternProperties): | |
398 | for k, v in iteritems(instance): |
|
241 | for k, v in iteritems(instance): | |
399 | if re.match(pattern, k): |
|
242 | if re.match(pattern, k): | |
400 | for error in self.iter_errors( |
|
243 | for error in self.iter_errors(v, subschema): | |
401 | v, subschema, meta_validate=False |
|
|||
402 | ): |
|
|||
403 | yield error |
|
244 | yield error | |
404 |
|
245 | |||
405 | def validate_additionalProperties(self, aP, instance, schema): |
|
246 | def validate_additionalProperties(self, aP, instance, schema): | |
406 | if not self.is_type(instance, "object"): |
|
247 | if not self.is_type(instance, "object"): | |
407 | return |
|
248 | return | |
408 |
|
249 | |||
409 | # no viewkeys in <2.7, and pypy seems to fail on vk - vk anyhow, so... |
|
250 | extras = set(_find_additional_properties(instance, schema)) | |
410 | extras = set(instance) - set(schema.get("properties", {})) |
|
|||
411 |
|
251 | |||
412 | if self.is_type(aP, "object"): |
|
252 | if self.is_type(aP, "object"): | |
413 | for extra in extras: |
|
253 | for extra in extras: | |
414 | for error in self.iter_errors( |
|
254 | for error in self.iter_errors(instance[extra], aP): | |
415 | instance[extra], aP, meta_validate=False |
|
|||
416 | ): |
|
|||
417 | yield error |
|
255 | yield error | |
418 | elif not aP and extras: |
|
256 | elif not aP and extras: | |
419 | error = "Additional properties are not allowed (%s %s unexpected)" |
|
257 | error = "Additional properties are not allowed (%s %s unexpected)" | |
420 | yield ValidationError(error % _extras_msg(extras)) |
|
258 | yield ValidationError(error % _extras_msg(extras)) | |
421 |
|
259 | |||
|
260 | def validate_dependencies(self, dependencies, instance, schema): | |||
|
261 | if not self.is_type(instance, "object"): | |||
|
262 | return | |||
|
263 | ||||
|
264 | for property, dependency in iteritems(dependencies): | |||
|
265 | if property not in instance: | |||
|
266 | continue | |||
|
267 | ||||
|
268 | if self.is_type(dependency, "object"): | |||
|
269 | for error in self.iter_errors(instance, dependency): | |||
|
270 | yield error | |||
|
271 | else: | |||
|
272 | dependencies = _list(dependency) | |||
|
273 | for dependency in dependencies: | |||
|
274 | if dependency not in instance: | |||
|
275 | yield ValidationError( | |||
|
276 | "%r is a dependency of %r" % (dependency, property) | |||
|
277 | ) | |||
|
278 | ||||
422 | def validate_items(self, items, instance, schema): |
|
279 | def validate_items(self, items, instance, schema): | |
423 | if not self.is_type(instance, "array"): |
|
280 | if not self.is_type(instance, "array"): | |
424 | return |
|
281 | return | |
425 |
|
282 | |||
426 | if self.is_type(items, "object"): |
|
283 | if self.is_type(items, "object"): | |
427 | for index, item in enumerate(instance): |
|
284 | for index, item in enumerate(instance): | |
428 | for error in self.iter_errors( |
|
285 | for error in self.iter_errors(item, items): | |
429 | item, items, meta_validate=False |
|
|||
430 | ): |
|
|||
431 | error.path.append(index) |
|
286 | error.path.append(index) | |
432 | yield error |
|
287 | yield error | |
433 | else: |
|
288 | else: | |
434 | for (index, item), subschema in zip(enumerate(instance), items): |
|
289 | for (index, item), subschema in zip(enumerate(instance), items): | |
435 | for error in self.iter_errors( |
|
290 | for error in self.iter_errors(item, subschema): | |
436 | item, subschema, meta_validate=False |
|
|||
437 | ): |
|
|||
438 | error.path.append(index) |
|
291 | error.path.append(index) | |
439 | yield error |
|
292 | yield error | |
440 |
|
293 | |||
441 | def validate_additionalItems(self, aI, instance, schema): |
|
294 | def validate_additionalItems(self, aI, instance, schema): | |
442 | if not self.is_type(instance, "array"): |
|
295 | if not self.is_type(instance, "array"): | |
443 | return |
|
296 | return | |
|
297 | if not self.is_type(schema.get("items"), "array"): | |||
|
298 | return | |||
444 |
|
299 | |||
445 | if self.is_type(aI, "object"): |
|
300 | if self.is_type(aI, "object"): | |
446 | for item in instance[len(schema):]: |
|
301 | for item in instance[len(schema):]: | |
447 |
for error in self.iter_errors(item, aI |
|
302 | for error in self.iter_errors(item, aI): | |
448 | yield error |
|
303 | yield error | |
449 | elif not aI and len(instance) > len(schema.get("items", [])): |
|
304 | elif not aI and len(instance) > len(schema.get("items", [])): | |
450 | error = "Additional items are not allowed (%s %s unexpected)" |
|
305 | error = "Additional items are not allowed (%s %s unexpected)" | |
451 | yield ValidationError( |
|
306 | yield ValidationError( | |
452 |
error % _extras_msg(instance[len(schema) |
|
307 | error % _extras_msg(instance[len(schema.get("items", [])):]) | |
453 | ) |
|
308 | ) | |
454 |
|
309 | |||
455 | def validate_minimum(self, minimum, instance, schema): |
|
310 | def validate_minimum(self, minimum, instance, schema): | |
456 | if not self.is_type(instance, "number"): |
|
311 | if not self.is_type(instance, "number"): | |
457 | return |
|
312 | return | |
458 |
|
313 | |||
459 | instance = float(instance) |
|
314 | instance = float(instance) | |
460 | if schema.get("exclusiveMinimum", False): |
|
315 | if schema.get("exclusiveMinimum", False): | |
461 | failed = instance <= minimum |
|
316 | failed = instance <= minimum | |
462 | cmp = "less than or equal to" |
|
317 | cmp = "less than or equal to" | |
463 | else: |
|
318 | else: | |
464 | failed = instance < minimum |
|
319 | failed = instance < minimum | |
465 | cmp = "less than" |
|
320 | cmp = "less than" | |
466 |
|
321 | |||
467 | if failed: |
|
322 | if failed: | |
468 | yield ValidationError( |
|
323 | yield ValidationError( | |
469 | "%r is %s the minimum of %r" % (instance, cmp, minimum) |
|
324 | "%r is %s the minimum of %r" % (instance, cmp, minimum) | |
470 | ) |
|
325 | ) | |
471 |
|
326 | |||
472 | def validate_maximum(self, maximum, instance, schema): |
|
327 | def validate_maximum(self, maximum, instance, schema): | |
473 | if not self.is_type(instance, "number"): |
|
328 | if not self.is_type(instance, "number"): | |
474 | return |
|
329 | return | |
475 |
|
330 | |||
476 | instance = float(instance) |
|
331 | instance = float(instance) | |
477 | if schema.get("exclusiveMaximum", False): |
|
332 | if schema.get("exclusiveMaximum", False): | |
478 | failed = instance >= maximum |
|
333 | failed = instance >= maximum | |
479 | cmp = "greater than or equal to" |
|
334 | cmp = "greater than or equal to" | |
480 | else: |
|
335 | else: | |
481 | failed = instance > maximum |
|
336 | failed = instance > maximum | |
482 | cmp = "greater than" |
|
337 | cmp = "greater than" | |
483 |
|
338 | |||
484 | if failed: |
|
339 | if failed: | |
485 | yield ValidationError( |
|
340 | yield ValidationError( | |
486 | "%r is %s the maximum of %r" % (instance, cmp, maximum) |
|
341 | "%r is %s the maximum of %r" % (instance, cmp, maximum) | |
487 | ) |
|
342 | ) | |
488 |
|
343 | |||
489 | def validate_minItems(self, mI, instance, schema): |
|
344 | def validate_minItems(self, mI, instance, schema): | |
490 | if self.is_type(instance, "array") and len(instance) < mI: |
|
345 | if self.is_type(instance, "array") and len(instance) < mI: | |
491 | yield ValidationError("%r is too short" % (instance,)) |
|
346 | yield ValidationError("%r is too short" % (instance,)) | |
492 |
|
347 | |||
493 | def validate_maxItems(self, mI, instance, schema): |
|
348 | def validate_maxItems(self, mI, instance, schema): | |
494 | if self.is_type(instance, "array") and len(instance) > mI: |
|
349 | if self.is_type(instance, "array") and len(instance) > mI: | |
495 | yield ValidationError("%r is too long" % (instance,)) |
|
350 | yield ValidationError("%r is too long" % (instance,)) | |
496 |
|
351 | |||
497 | def validate_uniqueItems(self, uI, instance, schema): |
|
352 | def validate_uniqueItems(self, uI, instance, schema): | |
498 | if uI and self.is_type(instance, "array") and not _uniq(instance): |
|
353 | if uI and self.is_type(instance, "array") and not _uniq(instance): | |
499 | yield ValidationError("%r has non-unique elements" % instance) |
|
354 | yield ValidationError("%r has non-unique elements" % instance) | |
500 |
|
355 | |||
501 | def validate_pattern(self, patrn, instance, schema): |
|
356 | def validate_pattern(self, patrn, instance, schema): | |
502 | if self.is_type(instance, "string") and not re.match(patrn, instance): |
|
357 | if self.is_type(instance, "string") and not re.match(patrn, instance): | |
503 | yield ValidationError("%r does not match %r" % (instance, patrn)) |
|
358 | yield ValidationError("%r does not match %r" % (instance, patrn)) | |
504 |
|
359 | |||
505 | def validate_minLength(self, mL, instance, schema): |
|
360 | def validate_minLength(self, mL, instance, schema): | |
506 | if self.is_type(instance, "string") and len(instance) < mL: |
|
361 | if self.is_type(instance, "string") and len(instance) < mL: | |
507 | yield ValidationError("%r is too short" % (instance,)) |
|
362 | yield ValidationError("%r is too short" % (instance,)) | |
508 |
|
363 | |||
509 | def validate_maxLength(self, mL, instance, schema): |
|
364 | def validate_maxLength(self, mL, instance, schema): | |
510 | if self.is_type(instance, "string") and len(instance) > mL: |
|
365 | if self.is_type(instance, "string") and len(instance) > mL: | |
511 | yield ValidationError("%r is too long" % (instance,)) |
|
366 | yield ValidationError("%r is too long" % (instance,)) | |
512 |
|
367 | |||
513 | def validate_enum(self, enums, instance, schema): |
|
368 | def validate_enum(self, enums, instance, schema): | |
514 | if instance not in enums: |
|
369 | if instance not in enums: | |
515 | yield ValidationError("%r is not one of %r" % (instance, enums)) |
|
370 | yield ValidationError("%r is not one of %r" % (instance, enums)) | |
516 |
|
371 | |||
517 | def validate_divisibleBy(self, dB, instance, schema): |
|
372 | def validate_divisibleBy(self, dB, instance, schema): | |
518 | if not self.is_type(instance, "number"): |
|
373 | if not self.is_type(instance, "number"): | |
519 | return |
|
374 | return | |
520 |
|
375 | |||
521 | if isinstance(dB, float): |
|
376 | if isinstance(dB, float): | |
522 | mod = instance % dB |
|
377 | mod = instance % dB | |
523 |
failed = (mod > |
|
378 | failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE | |
524 | else: |
|
379 | else: | |
525 | failed = instance % dB |
|
380 | failed = instance % dB | |
526 |
|
381 | |||
527 | if failed: |
|
382 | if failed: | |
528 | yield ValidationError("%r is not divisible by %r" % (instance, dB)) |
|
383 | yield ValidationError("%r is not divisible by %r" % (instance, dB)) | |
529 |
|
384 | |||
530 | def validate_disallow(self, disallow, instance, schema): |
|
385 | def validate_disallow(self, disallow, instance, schema): | |
531 | for disallowed in _list(disallow): |
|
386 | for disallowed in _list(disallow): | |
532 | if self.is_valid(instance, {"type" : [disallowed]}): |
|
387 | if self.is_valid(instance, {"type" : [disallowed]}): | |
533 | yield ValidationError( |
|
388 | yield ValidationError( | |
534 | "%r is disallowed for %r" % (disallowed, instance) |
|
389 | "%r is disallowed for %r" % (disallowed, instance) | |
535 | ) |
|
390 | ) | |
536 |
|
391 | |||
537 | def validate_extends(self, extends, instance, schema): |
|
392 | def validate_extends(self, extends, instance, schema): | |
538 | if self.is_type(extends, "object"): |
|
393 | if self.is_type(extends, "object"): | |
539 | extends = [extends] |
|
394 | extends = [extends] | |
540 | for subschema in extends: |
|
395 | for subschema in extends: | |
541 | for error in self.iter_errors( |
|
396 | for error in self.iter_errors(instance, subschema): | |
542 | instance, subschema, meta_validate=False |
|
|||
543 | ): |
|
|||
544 | yield error |
|
397 | yield error | |
545 |
|
398 | |||
|
399 | def validate_ref(self, ref, instance, schema): | |||
|
400 | if ref != "#" and not ref.startswith("#/"): | |||
|
401 | warnings.warn("jsonschema only supports json-pointer $refs") | |||
|
402 | return | |||
546 |
|
403 | |||
547 | for no_op in [ # handled in: |
|
404 | resolved = resolve_json_pointer(self.schema, ref) | |
548 | "dependencies", "required", # properties |
|
405 | for error in self.iter_errors(instance, resolved): | |
549 | "exclusiveMinimum", "exclusiveMaximum", # min*/max* |
|
406 | yield error | |
550 | "default", "description", "format", "id", # no validation needed |
|
407 | ||
551 | "links", "name", "title", |
|
408 | ||
552 | "ref", "schema", # not yet supported |
|
409 | Draft3Validator.META_SCHEMA = { | |
553 | ]: |
|
410 | "$schema" : "http://json-schema.org/draft-03/schema#", | |
554 | setattr(Validator, "validate_" + no_op, lambda *args, **kwargs : None) |
|
411 | "id" : "http://json-schema.org/draft-03/schema#", | |
|
412 | "type" : "object", | |||
|
413 | ||||
|
414 | "properties" : { | |||
|
415 | "type" : { | |||
|
416 | "type" : ["string", "array"], | |||
|
417 | "items" : {"type" : ["string", {"$ref" : "#"}]}, | |||
|
418 | "uniqueItems" : True, | |||
|
419 | "default" : "any" | |||
|
420 | }, | |||
|
421 | "properties" : { | |||
|
422 | "type" : "object", | |||
|
423 | "additionalProperties" : {"$ref" : "#", "type": "object"}, | |||
|
424 | "default" : {} | |||
|
425 | }, | |||
|
426 | "patternProperties" : { | |||
|
427 | "type" : "object", | |||
|
428 | "additionalProperties" : {"$ref" : "#"}, | |||
|
429 | "default" : {} | |||
|
430 | }, | |||
|
431 | "additionalProperties" : { | |||
|
432 | "type" : [{"$ref" : "#"}, "boolean"], "default" : {} | |||
|
433 | }, | |||
|
434 | "items" : { | |||
|
435 | "type" : [{"$ref" : "#"}, "array"], | |||
|
436 | "items" : {"$ref" : "#"}, | |||
|
437 | "default" : {} | |||
|
438 | }, | |||
|
439 | "additionalItems" : { | |||
|
440 | "type" : [{"$ref" : "#"}, "boolean"], "default" : {} | |||
|
441 | }, | |||
|
442 | "required" : {"type" : "boolean", "default" : False}, | |||
|
443 | "dependencies" : { | |||
|
444 | "type" : ["string", "array", "object"], | |||
|
445 | "additionalProperties" : { | |||
|
446 | "type" : ["string", "array", {"$ref" : "#"}], | |||
|
447 | "items" : {"type" : "string"} | |||
|
448 | }, | |||
|
449 | "default" : {} | |||
|
450 | }, | |||
|
451 | "minimum" : {"type" : "number"}, | |||
|
452 | "maximum" : {"type" : "number"}, | |||
|
453 | "exclusiveMinimum" : {"type" : "boolean", "default" : False}, | |||
|
454 | "exclusiveMaximum" : {"type" : "boolean", "default" : False}, | |||
|
455 | "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0}, | |||
|
456 | "maxItems" : {"type" : "integer", "minimum" : 0}, | |||
|
457 | "uniqueItems" : {"type" : "boolean", "default" : False}, | |||
|
458 | "pattern" : {"type" : "string", "format" : "regex"}, | |||
|
459 | "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0}, | |||
|
460 | "maxLength" : {"type" : "integer"}, | |||
|
461 | "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True}, | |||
|
462 | "default" : {"type" : "any"}, | |||
|
463 | "title" : {"type" : "string"}, | |||
|
464 | "description" : {"type" : "string"}, | |||
|
465 | "format" : {"type" : "string"}, | |||
|
466 | "maxDecimal" : {"type" : "number", "minimum" : 0}, | |||
|
467 | "divisibleBy" : { | |||
|
468 | "type" : "number", | |||
|
469 | "minimum" : 0, | |||
|
470 | "exclusiveMinimum" : True, | |||
|
471 | "default" : 1 | |||
|
472 | }, | |||
|
473 | "disallow" : { | |||
|
474 | "type" : ["string", "array"], | |||
|
475 | "items" : {"type" : ["string", {"$ref" : "#"}]}, | |||
|
476 | "uniqueItems" : True | |||
|
477 | }, | |||
|
478 | "extends" : { | |||
|
479 | "type" : [{"$ref" : "#"}, "array"], | |||
|
480 | "items" : {"$ref" : "#"}, | |||
|
481 | "default" : {} | |||
|
482 | }, | |||
|
483 | "id" : {"type" : "string", "format" : "uri"}, | |||
|
484 | "$ref" : {"type" : "string", "format" : "uri"}, | |||
|
485 | "$schema" : {"type" : "string", "format" : "uri"}, | |||
|
486 | }, | |||
|
487 | "dependencies" : { | |||
|
488 | "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum" | |||
|
489 | }, | |||
|
490 | } | |||
|
491 | ||||
|
492 | ||||
|
493 | class Validator(Draft3Validator): | |||
|
494 | """ | |||
|
495 | Deprecated: Use :class:`Draft3Validator` instead. | |||
|
496 | ||||
|
497 | """ | |||
|
498 | ||||
|
499 | def __init__( | |||
|
500 | self, version=None, unknown_type="skip", unknown_property="skip", | |||
|
501 | *args, **kwargs | |||
|
502 | ): | |||
|
503 | super(Validator, self).__init__({}, *args, **kwargs) | |||
|
504 | warnings.warn( | |||
|
505 | "Validator is deprecated and will be removed. " | |||
|
506 | "Use Draft3Validator instead.", | |||
|
507 | DeprecationWarning, stacklevel=2, | |||
|
508 | ) | |||
555 |
|
509 | |||
556 |
|
510 | |||
557 | class ErrorTree(object): |
|
511 | class ErrorTree(object): | |
558 | """ |
|
512 | """ | |
559 | ErrorTrees make it easier to check which validations failed. |
|
513 | ErrorTrees make it easier to check which validations failed. | |
560 |
|
514 | |||
561 | """ |
|
515 | """ | |
562 |
|
516 | |||
563 | def __init__(self, errors=()): |
|
517 | def __init__(self, errors=()): | |
564 | self.errors = {} |
|
518 | self.errors = {} | |
565 | self._contents = collections.defaultdict(self.__class__) |
|
519 | self._contents = collections.defaultdict(self.__class__) | |
566 |
|
520 | |||
567 | for error in errors: |
|
521 | for error in errors: | |
568 | container = self |
|
522 | container = self | |
569 | for element in reversed(error.path): |
|
523 | for element in reversed(error.path): | |
570 | container = container[element] |
|
524 | container = container[element] | |
571 | container.errors[error.validator] = error |
|
525 | container.errors[error.validator] = error | |
572 |
|
526 | |||
573 | def __contains__(self, k): |
|
527 | def __contains__(self, k): | |
574 | return k in self._contents |
|
528 | return k in self._contents | |
575 |
|
529 | |||
576 | def __getitem__(self, k): |
|
530 | def __getitem__(self, k): | |
577 | return self._contents[k] |
|
531 | return self._contents[k] | |
578 |
|
532 | |||
579 | def __setitem__(self, k, v): |
|
533 | def __setitem__(self, k, v): | |
580 | self._contents[k] = v |
|
534 | self._contents[k] = v | |
581 |
|
535 | |||
582 | def __iter__(self): |
|
536 | def __iter__(self): | |
583 | return iter(self._contents) |
|
537 | return iter(self._contents) | |
584 |
|
538 | |||
585 | def __len__(self): |
|
539 | def __len__(self): | |
586 | child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) |
|
540 | child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) | |
587 | return len(self.errors) + child_errors |
|
541 | return len(self.errors) + child_errors | |
588 |
|
542 | |||
589 | def __repr__(self): |
|
543 | def __repr__(self): | |
590 | return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) |
|
544 | return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) | |
591 |
|
545 | |||
592 |
|
546 | |||
|
547 | def resolve_json_pointer(schema, ref): | |||
|
548 | """ | |||
|
549 | Resolve a local reference ``ref`` within the given root ``schema``. | |||
|
550 | ||||
|
551 | ``ref`` should be a local ref whose ``#`` is still present. | |||
|
552 | ||||
|
553 | """ | |||
|
554 | ||||
|
555 | if ref == "#": | |||
|
556 | return schema | |||
|
557 | ||||
|
558 | parts = ref.lstrip("#/").split("/") | |||
|
559 | ||||
|
560 | parts = map(unquote, parts) | |||
|
561 | parts = [part.replace('~1', '/').replace('~0', '~') for part in parts] | |||
|
562 | ||||
|
563 | try: | |||
|
564 | for part in parts: | |||
|
565 | schema = schema[part] | |||
|
566 | except KeyError: | |||
|
567 | raise InvalidRef("Unresolvable json-pointer %r" % ref) | |||
|
568 | else: | |||
|
569 | return schema | |||
|
570 | ||||
|
571 | ||||
|
572 | def _find_additional_properties(instance, schema): | |||
|
573 | """ | |||
|
574 | Return the set of additional properties for the given ``instance``. | |||
|
575 | ||||
|
576 | Weeds out properties that should have been validated by ``properties`` and | |||
|
577 | / or ``patternProperties``. | |||
|
578 | ||||
|
579 | Assumes ``instance`` is dict-like already. | |||
|
580 | ||||
|
581 | """ | |||
|
582 | ||||
|
583 | properties = schema.get("properties", {}) | |||
|
584 | patterns = "|".join(schema.get("patternProperties", {})) | |||
|
585 | for property in instance: | |||
|
586 | if property not in properties: | |||
|
587 | if patterns and re.search(patterns, property): | |||
|
588 | continue | |||
|
589 | yield property | |||
|
590 | ||||
|
591 | ||||
593 | def _extras_msg(extras): |
|
592 | def _extras_msg(extras): | |
594 | """ |
|
593 | """ | |
595 | Create an error message for extra items or properties. |
|
594 | Create an error message for extra items or properties. | |
596 |
|
595 | |||
597 | """ |
|
596 | """ | |
598 |
|
597 | |||
599 | if len(extras) == 1: |
|
598 | if len(extras) == 1: | |
600 | verb = "was" |
|
599 | verb = "was" | |
601 | else: |
|
600 | else: | |
602 | verb = "were" |
|
601 | verb = "were" | |
603 | return ", ".join(repr(extra) for extra in extras), verb |
|
602 | return ", ".join(repr(extra) for extra in extras), verb | |
604 |
|
603 | |||
605 |
|
604 | |||
|
605 | def _types_msg(instance, types): | |||
|
606 | """ | |||
|
607 | Create an error message for a failure to match the given types. | |||
|
608 | ||||
|
609 | If the ``instance`` is an object and contains a ``name`` property, it will | |||
|
610 | be considered to be a description of that object and used as its type. | |||
|
611 | ||||
|
612 | Otherwise the message is simply the reprs of the given ``types``. | |||
|
613 | ||||
|
614 | """ | |||
|
615 | ||||
|
616 | reprs = [] | |||
|
617 | for type in types: | |||
|
618 | try: | |||
|
619 | reprs.append(repr(type["name"])) | |||
|
620 | except Exception: | |||
|
621 | reprs.append(repr(type)) | |||
|
622 | return "%r is not of type %s" % (instance, ", ".join(reprs)) | |||
|
623 | ||||
|
624 | ||||
|
625 | def _flatten(suitable_for_isinstance): | |||
|
626 | """ | |||
|
627 | isinstance() can accept a bunch of really annoying different types: | |||
|
628 | * a single type | |||
|
629 | * a tuple of types | |||
|
630 | * an arbitrary nested tree of tuples | |||
|
631 | ||||
|
632 | Return a flattened tuple of the given argument. | |||
|
633 | ||||
|
634 | """ | |||
|
635 | ||||
|
636 | types = set() | |||
|
637 | ||||
|
638 | if not isinstance(suitable_for_isinstance, tuple): | |||
|
639 | suitable_for_isinstance = (suitable_for_isinstance,) | |||
|
640 | for thing in suitable_for_isinstance: | |||
|
641 | if isinstance(thing, tuple): | |||
|
642 | types.update(_flatten(thing)) | |||
|
643 | else: | |||
|
644 | types.add(thing) | |||
|
645 | return tuple(types) | |||
|
646 | ||||
|
647 | ||||
606 | def _list(thing): |
|
648 | def _list(thing): | |
607 | """ |
|
649 | """ | |
608 | Wrap ``thing`` in a list if it's a single str. |
|
650 | Wrap ``thing`` in a list if it's a single str. | |
609 |
|
651 | |||
610 | Otherwise, return it unchanged. |
|
652 | Otherwise, return it unchanged. | |
611 |
|
653 | |||
612 | """ |
|
654 | """ | |
613 |
|
655 | |||
614 | if isinstance(thing, basestring): |
|
656 | if isinstance(thing, basestring): | |
615 | return [thing] |
|
657 | return [thing] | |
616 | return thing |
|
658 | return thing | |
617 |
|
659 | |||
618 |
|
660 | |||
619 | def _delist(thing): |
|
661 | def _delist(thing): | |
620 | """ |
|
662 | """ | |
621 | Unwrap ``thing`` to a single element if its a single str in a list. |
|
663 | Unwrap ``thing`` to a single element if its a single str in a list. | |
622 |
|
664 | |||
623 | Otherwise, return it unchanged. |
|
665 | Otherwise, return it unchanged. | |
624 |
|
666 | |||
625 | """ |
|
667 | """ | |
626 |
|
668 | |||
627 | if ( |
|
669 | if ( | |
628 | isinstance(thing, list) and |
|
670 | isinstance(thing, list) and | |
629 | len(thing) == 1 |
|
671 | len(thing) == 1 | |
630 | and isinstance(thing[0], basestring) |
|
672 | and isinstance(thing[0], basestring) | |
631 | ): |
|
673 | ): | |
632 | return thing[0] |
|
674 | return thing[0] | |
633 | return thing |
|
675 | return thing | |
634 |
|
676 | |||
635 |
|
677 | |||
636 | def validate( |
|
678 | def _uniq(container): | |
637 | instance, schema, meta_validate=True, cls=Validator, *args, **kwargs |
|
679 | """ | |
638 | ): |
|
680 | Check if all of a container's elements are unique. | |
|
681 | ||||
|
682 | Successively tries first to rely that the elements are hashable, then | |||
|
683 | falls back on them being sortable, and finally falls back on brute | |||
|
684 | force. | |||
|
685 | ||||
|
686 | """ | |||
|
687 | ||||
|
688 | try: | |||
|
689 | return len(set(container)) == len(container) | |||
|
690 | except TypeError: | |||
|
691 | try: | |||
|
692 | sort = sorted(container) | |||
|
693 | sliced = itertools.islice(container, 1, None) | |||
|
694 | for i, j in zip(container, sliced): | |||
|
695 | if i == j: | |||
|
696 | return False | |||
|
697 | except (NotImplementedError, TypeError): | |||
|
698 | seen = [] | |||
|
699 | for e in container: | |||
|
700 | if e in seen: | |||
|
701 | return False | |||
|
702 | seen.append(e) | |||
|
703 | return True | |||
|
704 | ||||
|
705 | ||||
|
706 | def validate(instance, schema, cls=Draft3Validator, *args, **kwargs): | |||
639 | """ |
|
707 | """ | |
640 | Validate an ``instance`` under the given ``schema``. |
|
708 | Validate an ``instance`` under the given ``schema``. | |
641 |
|
709 | |||
642 | By default, the :class:`Validator` class from this module is used to |
|
710 | First verifies that the provided schema is itself valid, since not doing so | |
643 | perform the validation. To use another validator, pass it into the ``cls`` |
|
711 | can lead to less obvious failures when validating. If you know it is or | |
644 | argument. |
|
712 | don't care, use ``YourValidator(schema).validate(instance)`` directly | |
|
713 | instead (e.g. ``Draft3Validator``). | |||
645 |
|
714 | |||
646 | Any other provided positional and keyword arguments will be provided to the |
|
715 | ``cls`` is a validator class that will be used to validate the instance. | |
647 | ``cls``. See the :class:`Validator` class' docstring for details on the |
|
716 | By default this is a draft 3 validator. Any other provided positional and | |
648 | arguments it accepts. |
|
717 | keyword arguments will be provided to this class when constructing a | |
|
718 | validator. | |||
649 |
|
719 | |||
650 | """ |
|
720 | """ | |
651 |
|
721 | |||
652 | validator = cls(*args, **kwargs) |
|
722 | ||
653 | validator.validate(instance, schema, meta_validate=meta_validate) |
|
723 | meta_validate = kwargs.pop("meta_validate", None) | |
|
724 | ||||
|
725 | if meta_validate is not None: | |||
|
726 | warnings.warn( | |||
|
727 | "meta_validate is deprecated and will be removed. If you do not " | |||
|
728 | "want to validate a schema, use Draft3Validator.validate instead.", | |||
|
729 | DeprecationWarning, stacklevel=2, | |||
|
730 | ) | |||
|
731 | ||||
|
732 | if meta_validate is not False: # yes this is needed since True was default | |||
|
733 | cls.check_schema(schema) | |||
|
734 | cls(schema, *args, **kwargs).validate(instance) |
@@ -1,88 +1,88 | |||||
1 | #!/usr/bin/env python |
|
1 | #!/usr/bin/env python | |
2 | # -*- coding: utf8 -*- |
|
2 | # -*- coding: utf8 -*- | |
3 |
|
3 | |||
4 | from IPython.external.jsonschema import Validator, validate, ValidationError |
|
4 | from IPython.external.jsonschema import Draft3Validator, validate, ValidationError | |
5 | import IPython.external.jsonpointer as jsonpointer |
|
5 | import IPython.external.jsonpointer as jsonpointer | |
6 | import argparse |
|
6 | import argparse | |
7 | import traceback |
|
7 | import traceback | |
8 | import json |
|
8 | import json | |
9 |
|
9 | |||
10 | v = Validator(); |
|
|||
11 | def nbvalidate(nbjson, schema='v3.withref.json', key=None,verbose=True): |
|
10 | def nbvalidate(nbjson, schema='v3.withref.json', key=None,verbose=True): | |
12 | v3schema = resolve_ref(json.load(open(schema,'r'))) |
|
11 | v3schema = resolve_ref(json.load(open(schema,'r'))) | |
13 | if key : |
|
12 | if key : | |
14 | v3schema = jsonpointer.resolve_pointer(v3schema,key) |
|
13 | v3schema = jsonpointer.resolve_pointer(v3schema,key) | |
15 | errors = 0 |
|
14 | errors = 0 | |
16 | for error in v.iter_errors(nbjson, v3schema): |
|
15 | v = Draft3Validator(v3schema); | |
|
16 | for error in v.iter_errors(nbjson): | |||
17 | errors = errors + 1 |
|
17 | errors = errors + 1 | |
18 | if verbose: |
|
18 | if verbose: | |
19 | print(error) |
|
19 | print(error) | |
20 | return errors |
|
20 | return errors | |
21 |
|
21 | |||
22 | def resolve_ref(json, base=None): |
|
22 | def resolve_ref(json, base=None): | |
23 | """return a json with resolved internal references |
|
23 | """return a json with resolved internal references | |
24 |
|
24 | |||
25 | only support local reference to the same json |
|
25 | only support local reference to the same json | |
26 | """ |
|
26 | """ | |
27 | if not base : |
|
27 | if not base : | |
28 | base = json |
|
28 | base = json | |
29 |
|
29 | |||
30 | temp = None |
|
30 | temp = None | |
31 | if type(json) is list: |
|
31 | if type(json) is list: | |
32 | temp = []; |
|
32 | temp = []; | |
33 | for item in json: |
|
33 | for item in json: | |
34 | temp.append(resolve_ref(item, base=base)) |
|
34 | temp.append(resolve_ref(item, base=base)) | |
35 | elif type(json) is dict: |
|
35 | elif type(json) is dict: | |
36 | temp = {}; |
|
36 | temp = {}; | |
37 | for key,value in json.iteritems(): |
|
37 | for key,value in json.iteritems(): | |
38 | if key == '$ref': |
|
38 | if key == '$ref': | |
39 | return resolve_ref(jsonpointer.resolve_pointer(base,value), base=base) |
|
39 | return resolve_ref(jsonpointer.resolve_pointer(base,value), base=base) | |
40 | else : |
|
40 | else : | |
41 | temp[key]=resolve_ref(value, base=base) |
|
41 | temp[key]=resolve_ref(value, base=base) | |
42 | else : |
|
42 | else : | |
43 | return json |
|
43 | return json | |
44 | return temp |
|
44 | return temp | |
45 |
|
45 | |||
46 | def convert(namein, nameout, indent=2): |
|
46 | def convert(namein, nameout, indent=2): | |
47 | """resolve the references of namein, save the result in nameout""" |
|
47 | """resolve the references of namein, save the result in nameout""" | |
48 | jsn = None |
|
48 | jsn = None | |
49 | with open(namein) as file : |
|
49 | with open(namein) as file : | |
50 | jsn = json.load(file) |
|
50 | jsn = json.load(file) | |
51 | v = resolve_ref(jsn, base=jsn) |
|
51 | v = resolve_ref(jsn, base=jsn) | |
52 | x = jsonpointer.resolve_pointer(v, '/notebook') |
|
52 | x = jsonpointer.resolve_pointer(v, '/notebook') | |
53 | with open(nameout,'w') as file: |
|
53 | with open(nameout,'w') as file: | |
54 | json.dump(x,file,indent=indent) |
|
54 | json.dump(x,file,indent=indent) | |
55 |
|
55 | |||
56 |
|
56 | |||
57 | if __name__ == '__main__': |
|
57 | if __name__ == '__main__': | |
58 | parser = argparse.ArgumentParser() |
|
58 | parser = argparse.ArgumentParser() | |
59 | parser.add_argument('-s', '--schema', |
|
59 | parser.add_argument('-s', '--schema', | |
60 | type=str, default='v3.withref.json') |
|
60 | type=str, default='v3.withref.json') | |
61 |
|
61 | |||
62 | parser.add_argument('-k', '--key', |
|
62 | parser.add_argument('-k', '--key', | |
63 | type=str, default='/notebook', |
|
63 | type=str, default='/notebook', | |
64 | help='subkey to extract json schema from json file') |
|
64 | help='subkey to extract json schema from json file') | |
65 |
|
65 | |||
66 | parser.add_argument("-v", "--verbose", action="store_true", |
|
66 | parser.add_argument("-v", "--verbose", action="store_true", | |
67 | help="increase output verbosity") |
|
67 | help="increase output verbosity") | |
68 |
|
68 | |||
69 | parser.add_argument('filename', |
|
69 | parser.add_argument('filename', | |
70 | type=str, |
|
70 | type=str, | |
71 | help="file to validate", |
|
71 | help="file to validate", | |
72 | nargs='*', |
|
72 | nargs='*', | |
73 | metavar='names') |
|
73 | metavar='names') | |
74 |
|
74 | |||
75 | args = parser.parse_args() |
|
75 | args = parser.parse_args() | |
76 | for name in args.filename : |
|
76 | for name in args.filename : | |
77 | nerror = nbvalidate(json.load(open(name,'r')), |
|
77 | nerror = nbvalidate(json.load(open(name,'r')), | |
78 | schema=args.schema, |
|
78 | schema=args.schema, | |
79 | key=args.key, |
|
79 | key=args.key, | |
80 | verbose=args.verbose) |
|
80 | verbose=args.verbose) | |
81 | if nerror is 0: |
|
81 | if nerror is 0: | |
82 | print u"[Pass]",name |
|
82 | print u"[Pass]",name | |
83 | else : |
|
83 | else : | |
84 | print u"[ ]",name,'(%d)'%(nerror) |
|
84 | print u"[ ]",name,'(%d)'%(nerror) | |
85 | if args.verbose : |
|
85 | if args.verbose : | |
86 | print '==================================================' |
|
86 | print '==================================================' | |
87 |
|
87 | |||
88 |
|
88 |
General Comments 0
You need to be logged in to leave comments.
Login now