Merge lp:~zyga/linaro-python-json/schema-and-object-tracing into lp:linaro-python-json

Proposed by Zygmunt Krynicki
Status: Merged
Approved by: James Westby
Approved revision: 107
Merged at revision: 105
Proposed branch: lp:~zyga/linaro-python-json/schema-and-object-tracing
Merge into: lp:linaro-python-json
Diff against target: 884 lines (+410/-111)
2 files modified
linaro_json/schema.py (+251/-75)
linaro_json/tests/test_schema.py (+159/-36)
To merge this branch: bzr merge lp:~zyga/linaro-python-json/schema-and-object-tracing
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+40723@code.launchpad.net

Description of the change

This branch contains all the code necessary to make use of linaro-python-json in launch-control to validate our dashboard bundle schema.

There are two separate changes:

1) The big change is error reporting. Previously there was a rather verbose dunp of JSON and a murky message what failed to validate (previously in this branch, in launch-control it was even worse). This has been changed to have a very short and concrete message of what the failure is (most of the time it's a constant string, rarely parametrized by expected data type). To allow the user or software to look up the offending piece of data and schema there are two new properties on ValidationError: object_expr and schema_expr. Each one gives a JavaScript expression that if evaluated against the data or schema objects yields the part that failed to validate. It can be thought as extremely simple xpath query.

All existing unit tests are _extended_ (not changed) so that they inspect the ValidationError for the new_message, object_expr and schema_expr and compare them against expected good values. Reading the unit tests also gives a good overview of how the error reporting messages look like and that it's actually helpful and easy to find the relevant fragment of data.

2) The minor change that I just lumped together is minimal support for "format" schema. In particular the "data-time" format is now supported allowing us to validate data that should express JSON date time objects. The code is very small and comes with unit tests for both good and bad cases. More information about this feature is here: http://tools.ietf.org/html/draft-zyp-json-schema-02#section-5.20

To post a comment you must log in.
107. By Zygmunt Krynicki

Remove needless newline

Revision history for this message
James Westby (james-w) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'linaro_json/schema.py'
2--- linaro_json/schema.py 2010-11-09 14:35:30 +0000
3+++ linaro_json/schema.py 2010-11-12 14:13:07 +0000
4@@ -5,6 +5,7 @@
5
6 See: json-schema.org for details
7 """
8+import datetime
9 import decimal
10 import itertools
11 import re
12@@ -23,9 +24,33 @@
13
14 class ValidationError(ValueError):
15 """
16- A bug in the validated object prevents the program from working
17+ A bug in the validated object prevents the program from working.
18+
19+ The error instance has several interesting properties:
20+ :message: old and verbose message that contains less helpful
21+ message and lots of JSON data (deprecated).
22+ :new_message: short and concise message about the problem
23+ :object_expr: a JavaScript expression that evaluates to the
24+ object that failed to validate. The expression always starts
25+ with a root object called 'object'.
26+ :schema_expr: a JavaScript expression that evaluats to the
27+ schema that was checked at the time validation failed. The
28+ expression always starts with a root object called 'schema'.
29 """
30
31+ def __init__(self, message, new_message=None,
32+ object_expr=None, schema_expr=None):
33+ self.message = message
34+ self.new_message = new_message
35+ self.object_expr = object_expr
36+ self.schema_expr = schema_expr
37+
38+ def __cmp__(self, other):
39+ return cmp(self.message, other.message) or cmp(self.new_message, self.other_messge)
40+
41+ def __repr__(self):
42+ return self.message
43+
44
45 class Schema(object):
46 """
47@@ -391,7 +416,28 @@
48 }
49
50 def __init__(self):
51- self._obj_stack = []
52+ self._schema_stack = []
53+ self._object_stack = []
54+
55+ def _push_object(self, obj, path):
56+ self._object_stack.append((obj, path))
57+
58+ def _pop_object(self):
59+ self._object_stack.pop()
60+
61+ def _push_schema(self, schema, path):
62+ self._schema_stack.append((schema, path))
63+
64+ def _pop_schema(self):
65+ self._schema_stack.pop()
66+
67+ @property
68+ def _object(self):
69+ return self._object_stack[-1][0]
70+
71+ @property
72+ def _schema(self):
73+ return self._schema_stack[-1][0]
74
75 @classmethod
76 def validate(cls, schema, obj):
77@@ -407,33 +453,91 @@
78 raise ValueError(
79 "schema value {0!r} is not a Schema"
80 " object".format(schema))
81- cls()._validate_no_push(schema, obj)
82+ self = cls()
83+ self.validate_toplevel(schema, obj)
84 return True
85
86- def _validate(self, schema, obj):
87- self._obj_stack.append(obj)
88- try:
89- self._validate_no_push(schema, obj)
90- finally:
91- self._obj_stack.pop()
92-
93- def _validate_no_push(self, schema, obj):
94- self._validate_type(schema, obj)
95- self._validate_requires(schema, obj)
96+ def _get_object_expression(self):
97+ return "".join(map(lambda x: x[1], self._object_stack))
98+
99+ def _get_schema_expression(self):
100+ return "".join(map(lambda x: x[1], self._schema_stack))
101+
102+ def validate_toplevel(self, schema, obj):
103+ self._object_stack = []
104+ self._schema_stack = []
105+ self._push_schema(schema, "schema")
106+ self._push_object(obj, "object")
107+ self._validate()
108+ self._pop_schema()
109+ self._pop_object()
110+
111+ def _validate(self):
112+ obj = self._object
113+ self._validate_type()
114+ self._validate_requires()
115 if isinstance(obj, dict):
116- self._obj_stack.append(obj)
117- self._validate_properties(schema, obj)
118- self._validate_additional_properties(schema, obj)
119- self._obj_stack.pop()
120+ self._validate_properties()
121+ self._validate_additional_properties()
122 elif isinstance(obj, list):
123- self._obj_stack.append(obj)
124- self._validate_items(schema, obj)
125- self._obj_stack.pop()
126+ self._validate_items()
127 else:
128- self._validate_enum(schema, obj)
129- self._report_unsupported(schema)
130-
131- def _report_unsupported(self, schema):
132+ self._validate_enum()
133+ self._validate_format()
134+ self._report_unsupported()
135+
136+ def _report_error(self, legacy_message, new_message=None,
137+ object_suffix=None, schema_suffix=None):
138+ """
139+ Report an error during validation.
140+
141+ There are two error messages. The legacy message is used for
142+ backwards compatibility and usually contains the object
143+ (possibly very large) that failed to validate. The new message
144+ is much better as it contains just a short message on what went
145+ wrong. User code can inspect object_expr and schema_expr to see
146+ which part of the object failed to validate against which part
147+ of the schema.
148+
149+ The schema_suffix, if provided, is appended to the schema_expr.
150+ This is quite handy to specify the bit that the validator looked
151+ at (such as the type or optional flag, etc). object_suffix
152+ serves the same purpose but is used for object expressions
153+ instead.
154+ """
155+ object_expr = self._get_object_expression()
156+ if object_suffix:
157+ object_expr += object_suffix
158+ schema_expr = self._get_schema_expression()
159+ if schema_suffix:
160+ schema_expr += schema_suffix
161+ raise ValidationError(legacy_message, new_message,
162+ object_expr, schema_expr)
163+
164+ def _push_property_schema(self, prop):
165+ """
166+ Construct a sub-schema from the value of the specified attribute
167+ of the current schema.
168+ """
169+ schema = Schema(self._schema.properties[prop])
170+ self._push_schema(schema, ".properties." + prop)
171+
172+ def _push_additional_property_schema(self):
173+ schema = Schema(self._schema.additionalProperties)
174+ self._push_schema(schema, ".additionalProperties")
175+
176+ def _push_array_schema(self):
177+ schema = Schema(self._schema.items)
178+ self._push_schema(schema, ".items")
179+
180+ def _push_array_item_object(self, index):
181+ self._push_object(self._object[index], "[%d]" % index)
182+
183+ def _push_property_object(self, prop):
184+ self._push_object(self._object[prop], "." + prop)
185+
186+ def _report_unsupported(self):
187+ schema = self._schema
188 if schema.minimum is not None:
189 raise NotImplementedError("minimum is not supported")
190 if schema.maximum is not None:
191@@ -450,8 +554,6 @@
192 raise NotImplementedError("minLength is not supported")
193 if schema.maxLength is not None:
194 raise NotImplementedError("maxLength is not supported")
195- if schema.format is not None:
196- raise NotImplementedError("format is not supported")
197 if schema.contentEncoding is not None:
198 raise NotImplementedError("contentEncoding is not supported")
199 if schema.divisibleBy != 1:
200@@ -459,7 +561,9 @@
201 if schema.disallow is not None:
202 raise NotImplementedError("disallow is not supported")
203
204- def _validate_type(self, schema, obj):
205+ def _validate_type(self):
206+ obj = self._object
207+ schema = self._schema
208 for json_type in schema.type:
209 if json_type == "any":
210 return
211@@ -468,14 +572,18 @@
212 # way to test for isinstance(something, bool) that would
213 # not catch isinstance(1, bool) :/
214 if obj is not True and obj is not False:
215- raise ValidationError(
216+ self._report_error(
217 "{obj!r} does not match type {type!r}".format(
218- obj=obj, type=json_type))
219+ obj=obj, type=json_type),
220+ "Object has incorrect type (expected boolean)",
221+ schema_suffix=".type")
222 break
223 elif isinstance(json_type, dict):
224 # Nested type check. This is pretty odd case. Here we
225 # don't change our object stack (it's the same object).
226- self._validate_no_push(Schema(json_type), obj)
227+ self._push_schema(Schema(json_type), ".type")
228+ self._validate()
229+ self._pop_schema()
230 break
231 else:
232 # Simple type check
233@@ -483,106 +591,172 @@
234 # First one that matches, wins
235 break
236 else:
237- raise ValidationError(
238+ self._report_error(
239 "{obj!r} does not match type {type!r}".format(
240- obj=obj, type=json_type))
241-
242- def _validate_properties(self, schema, obj):
243+ obj=obj, type=json_type),
244+ "Object has incorrect type (expected {type})".format(
245+ type=json_type),
246+ schema_suffix=".type")
247+
248+ def _validate_format(self):
249+ fmt = self._schema.format
250+ obj = self._object
251+ if fmt is None:
252+ return
253+ if fmt == 'date-time':
254+ try:
255+ DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
256+ datetime.datetime.strptime(obj, DATE_TIME_FORMAT)
257+ except ValueError:
258+ self._report_error(
259+ "{obj!r} is not a string representing JSON date-time".format(
260+ obj=obj),
261+ "Object is not a string representing JSON date-time",
262+ schema_suffix=".format")
263+ else:
264+ raise NotImplementedError("format {0!r} is not supported".format(format))
265+
266+ def _validate_properties(self):
267+ obj = self._object
268+ schema = self._schema
269 assert isinstance(obj, dict)
270- for prop, prop_schema_data in schema.properties.iteritems():
271- prop_schema = Schema(prop_schema_data)
272+ for prop in schema.properties.iterkeys():
273+ self._push_property_schema(prop)
274 if prop in obj:
275- self._validate(prop_schema, obj[prop])
276+ self._push_property_object(prop)
277+ self._validate()
278+ self._pop_object()
279 else:
280- if not prop_schema.optional:
281- raise ValidationError(
282- "{obj!r} does not have property"
283- " {prop!r}".format( obj=obj, prop=prop))
284+ if not self._schema.optional:
285+ self._report_error(
286+ "{obj!r} does not have property {prop!r}".format(
287+ obj=obj, prop=prop),
288+ "Object lacks property {prop!r}".format(
289+ prop=prop),
290+ schema_suffix=".optional")
291+ self._pop_schema()
292
293- def _validate_additional_properties(self, schema, obj):
294+ def _validate_additional_properties(self):
295+ obj = self._object
296 assert isinstance(obj, dict)
297- if schema.additionalProperties is False:
298+ if self._schema.additionalProperties is False:
299 # Additional properties are disallowed
300 # Report exception for each unknown property
301 for prop in obj.iterkeys():
302- if prop not in schema.properties:
303- raise ValidationError(
304+ if prop not in self._schema.properties:
305+ self._report_error(
306 "{obj!r} has unknown property {prop!r} and"
307 " additionalProperties is false".format(
308- obj=obj, prop=prop))
309+ obj=obj, prop=prop),
310+ "Object has unknown property {prop!r} but"
311+ " additional properties are disallowed".format(
312+ prop=prop),
313+ schema_suffix=".additionalProperties")
314 else:
315- additional_schema = Schema(schema.additionalProperties)
316 # Check each property against this object
317- for prop_value in obj.itervalues():
318- self._validate(additional_schema, prop_value)
319+ self._push_additional_property_schema()
320+ for prop in obj.iterkeys():
321+ self._push_property_object(prop)
322+ self._validate()
323+ self._pop_object()
324+ self._pop_schema()
325
326- def _validate_enum(self, schema, obj):
327+ def _validate_enum(self):
328+ obj = self._object
329+ schema = self._schema
330 if schema.enum is not None:
331 for allowed_value in schema.enum:
332 if obj == allowed_value:
333 break
334 else:
335- raise ValidationError(
336+ self._report_error(
337 "{obj!r} does not match any value in enumeration"
338- " {enum!r}".format(obj=obj, enum=schema.enum))
339+ " {enum!r}".format(obj=obj, enum=schema.enum),
340+ "Object does not match any value in enumeration",
341+ schema_suffix=".enum")
342
343- def _validate_items(self, schema, obj):
344+ def _validate_items(self):
345+ obj = self._object
346+ schema = self._schema
347 assert isinstance(obj, list)
348 items_schema_json = schema.items
349 if items_schema_json == {}:
350 # default value, don't do anything
351 return
352 if isinstance(items_schema_json, dict):
353- items_schema = Schema(items_schema_json)
354- for item in obj:
355- self._validate(items_schema, item)
356+ self._push_array_schema()
357+ for index, item in enumerate(obj):
358+ self._push_array_item_object(index)
359+ self._validate()
360+ self._pop_object()
361+ self._pop_schema()
362 elif isinstance(items_schema_json, list):
363 if len(obj) < len(items_schema_json):
364- # If our array is shorter than the schema then
365+ # If our data array is shorter than the schema then
366 # validation fails. Longer arrays are okay (during this
367 # step) as they are validated based on
368 # additionalProperties schema
369- raise ValidationError(
370+ self._report_error(
371 "{obj!r} is shorter than array schema {schema!r}".
372- format(obj=obj, schema=items_schema_json))
373+ format(obj=obj, schema=items_schema_json),
374+ "Object array is shorter than schema array",
375+ schema_suffix=".items")
376 if len(obj) != len(items_schema_json) and schema.additionalProperties is False:
377 # If our array is not exactly the same size as the
378 # schema and additional properties are disallowed then
379 # validation fails
380- raise ValidationError(
381+ self._report_error(
382 "{obj!r} is not of the same length as array schema"
383 " {schema!r} and additionalProperties is"
384- " false".format(obj=obj, schema=items_schema_json))
385+ " false".format(obj=obj, schema=items_schema_json),
386+ "Object array is not of the same length as schema array",
387+ schema_suffix=".items")
388 # Validate each array element using schema for the
389 # corresponding array index, fill missing values (since
390 # there may be more items in our array than in the schema)
391 # with additionalProperties which by now is not False
392- for item, item_schema_json in itertools.izip_longest(
393- obj, items_schema_json, fillvalue=schema.additionalProperties):
394+ for index, (item, item_schema_json) in enumerate(
395+ itertools.izip_longest(
396+ obj, items_schema_json,
397+ fillvalue=schema.additionalProperties)):
398 item_schema = Schema(item_schema_json)
399- self._validate(item_schema, item)
400+ if index < len(items_schema_json):
401+ self._push_schema(item_schema, "items[%d]" % index)
402+ else:
403+ self._push_schema(item_schema, ".additionalProperties")
404+ self._push_array_item_object(index)
405+ self._validate()
406+ self._pop_schema()
407+ self._pop_object()
408
409- def _validate_requires(self, schema, obj):
410+ def _validate_requires(self):
411+ obj = self._object
412+ schema = self._schema
413 requires_json = schema.requires
414 if requires_json == {}:
415 # default value, don't do anything
416 return
417 # Find our enclosing object in the object stack
418- if len(self._obj_stack) < 2:
419- raise ValidationError(
420+ if len(self._object_stack) < 2:
421+ self._report_error(
422 "{obj!r} requires that enclosing object matches"
423 " schema {schema!r} but there is no enclosing"
424- " object".format(obj=obj, schema=requires_json))
425+ " object".format(obj=obj, schema=requires_json),
426+ "Object has no enclosing object that matches schema",
427+ schema_suffix=".requires")
428 # Note: Parent object can be None, (e.g. a null property)
429- parent_obj = self._obj_stack[-2]
430+ parent_obj = self._object_stack[-2][0]
431 if isinstance(requires_json, basestring):
432 # This is a simple property test
433- if (not isinstance(parent_obj, dict)
434+ if (not isinstance(parent_obj, dict)
435 or requires_json not in parent_obj):
436- raise ValidationError(
437+ self._report_error(
438 "{obj!r} requires presence of property {requires!r}"
439 " in the same object".format(
440- obj=obj, requires=requires_json))
441+ obj=obj, requires=requires_json),
442+ "Enclosing object does not have property"
443+ " {prop!r}".format(prop=requires_json),
444+ schema_suffix=".requires")
445 elif isinstance(requires_json, dict):
446 # Requires designates a whole schema, the enclosing object
447 # must match against that schema.
448@@ -593,9 +767,11 @@
449 # instantiate a new validator with a subset of our current
450 # history here.
451 sub_validator = Validator()
452- sub_validator._obj_stack = self._obj_stack[:-2]
453- sub_validator._validate_no_push(
454- Schema(requires_json), parent_obj)
455+ sub_validator._object_stack = self._object_stack[:-1]
456+ sub_validator._schema_stack = self._schema_stack[:]
457+ sub_validator._push_schema(
458+ Schema(requires_json), ".requires")
459+ sub_validator._validate()
460
461
462 def validate(schema_text, data_text):
463
464=== modified file 'linaro_json/tests/test_schema.py'
465--- linaro_json/tests/test_schema.py 2010-11-09 14:35:41 +0000
466+++ linaro_json/tests/test_schema.py 2010-11-12 14:13:07 +0000
467@@ -757,39 +757,52 @@
468 'schema': '{"type": "string"}',
469 'data': 'null',
470 'raises': ValidationError(
471- "None does not match type 'string'"),
472+ "None does not match type 'string'",
473+ "Object has incorrect type (expected string)"),
474+ 'object_expr': 'object',
475+ 'schema_expr': 'schema.type',
476 }),
477 ("type_string_got_integer", {
478 'schema': '{"type": "string"}',
479 'data': '5',
480 'raises': ValidationError(
481- "5 does not match type 'string'"),
482+ "5 does not match type 'string'",
483+ "Object has incorrect type (expected string)"),
484+ 'object_expr': 'object',
485+ 'schema_expr': 'schema.type',
486 }),
487 ("type_number_got_integer", {
488 'schema': '{"type": "number"}',
489- 'data': '1'
490+ 'data': '1',
491 }),
492 ("type_number_number_float", {
493 'schema': '{"type": "number"}',
494- 'data': '1.1'
495+ 'data': '1.1',
496 }),
497 ("type_number_got_null", {
498 'schema': '{"type": "number"}',
499 'data': 'null',
500 'raises': ValidationError(
501- "None does not match type 'number'"),
502+ "None does not match type 'number'",
503+ "Object has incorrect type (expected number)"),
504+ 'object_expr': 'object',
505+ 'schema_expr': 'schema.type',
506 }),
507 ("type_number_got_string", {
508 'schema': '{"type": "number"}',
509 'data': '"foobar"',
510 'raises': ValidationError(
511 "'foobar' does not match type 'number'"),
512+ 'object_expr': 'object',
513+ 'schema_expr': 'schema.type',
514 }),
515 ("type_number_got_string_that_looks_like_number", {
516 'schema': '{"type": "number"}',
517 'data': '"3"',
518 'raises': ValidationError(
519 "'3' does not match type 'number'"),
520+ 'object_expr': 'object',
521+ 'schema_expr': 'schema.type',
522 }),
523 ("type_integer_got_integer_one", {
524 'schema': '{"type": "integer"}',
525@@ -803,13 +816,19 @@
526 'schema': '{"type": "integer"}',
527 'data': '1.1',
528 'raises': ValidationError(
529- "1.1000000000000001 does not match type 'integer'")
530+ "1.1000000000000001 does not match type 'integer'",
531+ "Object has incorrect type (expected integer)"),
532+ 'object_expr': 'object',
533+ 'schema_expr': 'schema.type',
534 }),
535 ("type_integer_got_null", {
536 'schema': '{"type": "integer"}',
537 'data': 'null',
538 'raises': ValidationError(
539- "None does not match type 'integer'")
540+ "None does not match type 'integer'",
541+ "Object has incorrect type (expected integer)"),
542+ 'object_expr': 'object',
543+ 'schema_expr': 'schema.type',
544 }),
545 ("type_boolean_got_true", {
546 'schema': '{"type": "boolean"}',
547@@ -823,25 +842,37 @@
548 'schema': '{"type": "boolean"}',
549 'data': 'null',
550 'raises': ValidationError(
551- "None does not match type 'boolean'"),
552+ "None does not match type 'boolean'",
553+ "Object has incorrect type (expected boolean)"),
554+ 'object_expr': 'object',
555+ 'schema_expr': 'schema.type',
556 }),
557 ("type_boolean_got_empty_string", {
558 'schema': '{"type": "boolean"}',
559 'data': '""',
560 'raises': ValidationError(
561- "'' does not match type 'boolean'"),
562+ "'' does not match type 'boolean'",
563+ "Object has incorrect type (expected boolean)"),
564+ 'object_expr': 'object',
565+ 'schema_expr': 'schema.type',
566 }),
567 ("type_boolean_got_empty_list", {
568 'schema': '{"type": "boolean"}',
569 'data': '[]',
570 'raises': ValidationError(
571- "[] does not match type 'boolean'"),
572+ "[] does not match type 'boolean'",
573+ "Object has incorrect type (expected boolean)"),
574+ 'object_expr': 'object',
575+ 'schema_expr': 'schema.type',
576 }),
577 ("type_boolean_got_empty_object", {
578 'schema': '{"type": "boolean"}',
579 'data': '{}',
580 'raises': ValidationError(
581- "{} does not match type 'boolean'"),
582+ "{} does not match type 'boolean'",
583+ "Object has incorrect type (expected boolean)"),
584+ 'object_expr': 'object',
585+ 'schema_expr': 'schema.type',
586 }),
587 ("type_object_got_object", {
588 'schema': '{"type": "object"}',
589@@ -851,13 +882,19 @@
590 'schema': '{"type": "object"}',
591 'data': '1',
592 'raises': ValidationError(
593- "1 does not match type 'object'")
594+ "1 does not match type 'object'",
595+ "Object has incorrect type (expected object)"),
596+ 'object_expr': 'object',
597+ 'schema_expr': 'schema.type',
598 }),
599 ("type_object_got_null", {
600 'schema': '{"type": "object"}',
601 'data': 'null',
602 'raises': ValidationError(
603- "None does not match type 'object'")
604+ "None does not match type 'object'",
605+ "Object has incorrect type (expected object)"),
606+ 'object_expr': 'object',
607+ 'schema_expr': 'schema.type',
608 }),
609 ("type_array_got_array", {
610 'schema': '{"type": "array"}',
611@@ -867,13 +904,19 @@
612 'schema': '{"type": "array"}',
613 'data': 'null',
614 'raises': ValidationError(
615- "None does not match type 'array'")
616+ "None does not match type 'array'",
617+ "Object has incorrect type (expected array)"),
618+ 'object_expr': 'object',
619+ 'schema_expr': 'schema.type',
620 }),
621 ("type_array_got_integer", {
622 'schema': '{"type": "array"}',
623 'data': '1',
624 'raises': ValidationError(
625- "1 does not match type 'array'")
626+ "1 does not match type 'array'",
627+ "Object has incorrect type (expected array)"),
628+ 'object_expr': 'object',
629+ 'schema_expr': 'schema.type',
630 }),
631 ("type_null_got_null", {
632 'schema': '{"type": "null"}',
633@@ -883,25 +926,37 @@
634 'schema': '{"type": "null"}',
635 'data': '""',
636 'raises': ValidationError(
637- "'' does not match type 'null'")
638+ "'' does not match type 'null'",
639+ "Object has incorrect type (expected null)"),
640+ 'object_expr': 'object',
641+ 'schema_expr': 'schema.type',
642 }),
643 ("type_null_got_zero", {
644 'schema': '{"type": "null"}',
645 'data': '0',
646 'raises': ValidationError(
647- "0 does not match type 'null'")
648+ "0 does not match type 'null'",
649+ "Object has incorrect type (expected null)"),
650+ 'object_expr': 'object',
651+ 'schema_expr': 'schema.type',
652 }),
653 ("type_null_got_empty_list", {
654 'schema': '{"type": "null"}',
655 'data': '[]',
656 'raises': ValidationError(
657- "[] does not match type 'null'")
658+ "[] does not match type 'null'",
659+ "Object has incorrect type (expected null)"),
660+ 'object_expr': 'object',
661+ 'schema_expr': 'schema.type',
662 }),
663 ("type_null_got_empty_object", {
664 'schema': '{"type": "null"}',
665 'data': '{}',
666 'raises': ValidationError(
667- "{} does not match type 'null'")
668+ "{} does not match type 'null'",
669+ "Object has incorrect type (expected null)"),
670+ 'object_expr': 'object',
671+ 'schema_expr': 'schema.type',
672 }),
673 ("type_any_got_null", {
674 'schema': '{"type": "any"}',
675@@ -966,7 +1021,10 @@
676 }""",
677 'data': '{"foo": "foobar"}',
678 'raises': ValidationError(
679- "'foobar' does not match type 'number'")
680+ "'foobar' does not match type 'number'",
681+ "Object has incorrect type (expected number)"),
682+ 'object_expr': 'object.foo',
683+ 'schema_expr': 'schema.properties.foo.type',
684 }),
685 ("property_check_ignores_missing_optional_properties", {
686 'schema': """
687@@ -994,7 +1052,10 @@
688 }""",
689 'data': '{"foo": null}',
690 'raises': ValidationError(
691- "None does not match type 'number'")
692+ "None does not match type 'number'",
693+ "Object has incorrect type (expected number)"),
694+ 'object_expr': 'object.foo',
695+ 'schema_expr': 'schema.properties.foo.type',
696 }),
697 ("property_check_reports_missing_non_optional_properties", {
698 'schema': """
699@@ -1009,7 +1070,10 @@
700 }""",
701 'data': '{}',
702 'raises': ValidationError(
703- "{} does not have property 'foo'")
704+ "{} does not have property 'foo'",
705+ "Object lacks property 'foo'"),
706+ 'object_expr': 'object',
707+ 'schema_expr': 'schema.properties.foo.optional',
708 }),
709 ("property_check_reports_unknown_properties_when_additionalProperties_is_false", {
710 'schema': """
711@@ -1020,7 +1084,11 @@
712 'data': '{"foo": 5}',
713 'raises': ValidationError(
714 "{'foo': 5} has unknown property 'foo' and"
715- " additionalProperties is false")
716+ " additionalProperties is false",
717+ "Object has unknown property 'foo' but additional "
718+ "properties are disallowed"),
719+ 'object_expr': 'object',
720+ 'schema_expr': 'schema.additionalProperties',
721 }),
722 ("property_check_ignores_normal_properties_when_additionalProperties_is_false", {
723 'schema': """
724@@ -1053,7 +1121,10 @@
725 }""",
726 'data': '{"foo": "aaa", "bar": 5}',
727 'raises': ValidationError(
728- "5 does not match type 'string'")
729+ "5 does not match type 'string'",
730+ "Object has incorrect type (expected string)"),
731+ 'object_expr': 'object.bar',
732+ 'schema_expr': 'schema.additionalProperties.type',
733 }),
734 ("enum_check_does_nothing_by_default", {
735 'schema': '{}',
736@@ -1067,7 +1138,10 @@
737 'schema': '{"enum": [1, 2, 3]}',
738 'data': '5',
739 'raises': ValidationError(
740- '5 does not match any value in enumeration [1, 2, 3]')
741+ '5 does not match any value in enumeration [1, 2, 3]',
742+ "Object does not match any value in enumeration"),
743+ 'object_expr': 'object',
744+ 'schema_expr': 'schema.enum',
745 }),
746 ("items_check_does_nothing_for_non_arrays", {
747 'schema': '{"items": {"type": "string"}}',
748@@ -1081,7 +1155,10 @@
749 'schema': '{"items": {"type": "string"}}',
750 'data': '["foo", null, "froz"]',
751 'raises': ValidationError(
752- "None does not match type 'string'")
753+ "None does not match type 'string'",
754+ "Object has incorrect type (expected string)"),
755+ 'object_expr': 'object[1]',
756+ 'schema_expr': 'schema.items.type',
757 }),
758 ("items_with_array_schema_applies_to_corresponding_items", {
759 'schema': """
760@@ -1104,7 +1181,10 @@
761 'data': '["foo"]',
762 'raises': ValidationError(
763 "['foo'] is shorter than array schema [{'type':"
764- " 'string'}, {'type': 'boolean'}]")
765+ " 'string'}, {'type': 'boolean'}]",
766+ "Object array is shorter than schema array"),
767+ 'object_expr': 'object',
768+ 'schema_expr': 'schema.items',
769 }),
770 ("items_with_array_schema_and_additionalProperties_of_false_checks_for_too_much_data", {
771 'schema': """
772@@ -1119,7 +1199,10 @@
773 'raises': ValidationError(
774 "['foo', False, 5] is not of the same length as array"
775 " schema [{'type': 'string'}, {'type': 'boolean'}] and"
776- " additionalProperties is false")
777+ " additionalProperties is false",
778+ "Object array is not of the same length as schema array"),
779+ 'object_expr': 'object',
780+ 'schema_expr': 'schema.items',
781 }),
782 ("items_with_array_schema_and_additionalProperties", {
783 'schema': """
784@@ -1147,7 +1230,10 @@
785 }""",
786 'data': '["foo", false, 5, 7.9, null]',
787 'raises': ValidationError(
788- "None does not match type 'number'")
789+ "None does not match type 'number'",
790+ "Object has incorrect type (expected number)"),
791+ 'object_expr': 'object[4]',
792+ 'schema_expr': 'schema.additionalProperties.type',
793 }),
794 ("requires_with_simple_property_name_does_nothing_when_parent_property_is_not_used", {
795 'schema': """
796@@ -1182,7 +1268,10 @@
797 'data': '{"bar": null}',
798 'raises': ValidationError(
799 "None requires presence of property 'foo' in the same"
800- " object")
801+ " object",
802+ "Enclosing object does not have property 'foo'"),
803+ 'object_expr': 'object.bar',
804+ 'schema_expr': 'schema.properties.bar.requires',
805 }),
806 ("requires_with_simple_property_name_can_report_problems_while_nested", {
807 'schema': """
808@@ -1206,7 +1295,10 @@
809 'data': '{"nested": {"bar": null}}',
810 'raises': ValidationError(
811 "None requires presence of property 'foo' in the same"
812- " object")
813+ " object",
814+ "Enclosing object does not have property 'foo'"),
815+ 'object_expr': 'object.nested.bar',
816+ 'schema_expr': 'schema.properties.nested.properties.bar.requires',
817 }),
818 ("requires_with_simple_property_name_works_when_condition_satisfied", {
819 'schema': """
820@@ -1268,7 +1360,10 @@
821 """,
822 'data': '{"bar": null}',
823 'raises': ValidationError(
824- "{'bar': None} does not have property 'foo'")
825+ "{'bar': None} does not have property 'foo'",
826+ "Object lacks property 'foo'"),
827+ 'object_expr': 'object',
828+ 'schema_expr': 'schema.properties.bar.requires.properties.foo.optional',
829 }),
830 ("requires_with_schema_can_report_subtle_problems", {
831 # In this test presence of "bar" requires that "foo" is
832@@ -1294,9 +1389,24 @@
833 """,
834 'data': '{"bar": null, "foo": "not a number"}',
835 'raises': ValidationError(
836- "'not a number' does not match type 'number'")
837- }),
838-
839+ "'not a number' does not match type 'number'",
840+ "Object has incorrect type (expected number)"),
841+ 'object_expr': 'object.foo',
842+ 'schema_expr': 'schema.properties.bar.requires.properties.foo.type'
843+ }),
844+ ("format_date_time_works", {
845+ 'schema': '{"format": "date-time"}',
846+ 'data': '"2010-11-12T14:38:55Z"',
847+ }),
848+ ("format_date_time_finds_problems", {
849+ 'schema': '{"format": "date-time"}',
850+ 'data': '"broken"',
851+ 'raises': ValidationError(
852+ "'broken' is not a string representing JSON date-time",
853+ "Object is not a string representing JSON date-time"),
854+ 'object_expr': 'object',
855+ 'schema_expr': 'schema.format'
856+ }),
857 ]
858
859 def test_validate(self):
860@@ -1306,10 +1416,23 @@
861 try:
862 validate(self.schema, self.data)
863 except type(self.raises) as ex:
864- self.assertEqual(str(ex), str(self.raises))
865+ self.assertEqual(ex.message, self.raises.message)
866+ if self.raises.new_message is not None:
867+ self.assertEqual(ex.new_message, self.raises.new_message)
868+ self.assertEqual(ex.object_expr, self.object_expr)
869+ self.assertEqual(ex.schema_expr, self.schema_expr)
870 except Exception as ex:
871 self.fail("Raised exception {0!r} instead of {1!r}".format(
872 ex, self.raises))
873 else:
874 self.assertEqual(
875 True, validate(self.schema, self.data))
876+
877+ def __str__(self):
878+ """
879+ Override TestCase to report the scenario name.
880+
881+ TODO: Replace this with TestCaseWithScenarios in subsequent pipe
882+ """
883+ return self.id()
884+

Subscribers

People subscribed via source and target branches

to all changes: