Skip to content

Commit 5f81f44

Browse files
committed
Add option to disable fast fail and return all the errors
1 parent ab94eac commit 5f81f44

File tree

8 files changed

+100
-21
lines changed

8 files changed

+100
-21
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jsonschemasuitcases:
2929
git submodule update
3030

3131
test: venv jsonschemasuitcases
32-
${PYTHON} -m pytest -W default --benchmark-skip #tests/json_schema/test_draft07.py
32+
${PYTHON} -m pytest -W default --benchmark-skip #tests/test_fast_fail.py
3333
test-lf: venv jsonschemasuitcases
3434
${PYTHON} -m pytest -W default --benchmark-skip --last-failed
3535

fastjsonschema/__init__.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,22 +110,23 @@
110110
from .draft04 import CodeGeneratorDraft04
111111
from .draft06 import CodeGeneratorDraft06
112112
from .draft07 import CodeGeneratorDraft07
113-
from .exceptions import JsonSchemaException, JsonSchemaValueException, JsonSchemaDefinitionException
113+
from .exceptions import JsonSchemaException, JsonSchemaValueException, JsonSchemaValuesException, JsonSchemaDefinitionException
114114
from .ref_resolver import RefResolver
115115
from .version import VERSION
116116

117117
__all__ = (
118118
'VERSION',
119119
'JsonSchemaException',
120120
'JsonSchemaValueException',
121+
'JsonSchemaValuesException',
121122
'JsonSchemaDefinitionException',
122123
'validate',
123124
'compile',
124125
'compile_to_code',
125126
)
126127

127128

128-
def validate(definition, data, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
129+
def validate(definition, data, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
129130
"""
130131
Validation function for lazy programmers or for use cases when you need
131132
to call validation only once, so you do not have to compile it first.
@@ -141,12 +142,12 @@ def validate(definition, data, handlers={}, formats={}, use_default=True, use_fo
141142
142143
Preferred is to use :any:`compile` function.
143144
"""
144-
return compile(definition, handlers, formats, use_default, use_formats, detailed_exceptions)(data)
145+
return compile(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)(data)
145146

146147

147148
#TODO: Change use_default to False when upgrading to version 3.
148149
# pylint: disable=redefined-builtin,dangerous-default-value,exec-used
149-
def compile(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
150+
def compile(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
150151
"""
151152
Generates validation function for validating JSON schema passed in ``definition``.
152153
Example:
@@ -205,13 +206,20 @@ def compile(definition, handlers={}, formats={}, use_default=True, use_formats=T
205206
If you don't need detailed exceptions, you can turn the details off and gain
206207
additional performance by passing `detailed_exceptions=False`.
207208
209+
By default, the execution stops with the first validation error. If you need
210+
to collect all the errors, turn this off by passing `fast_fail=False`.
211+
208212
Exception :any:`JsonSchemaDefinitionException` is raised when generating the
209213
code fails (bad definition).
210214
211215
Exception :any:`JsonSchemaValueException` is raised from generated function when
212216
validation fails (data do not follow the definition).
217+
218+
Exception :any:`JsonSchemaValuesException` is raised from generated function when
219+
validation fails (data do not follow the definition) contatining all the errors
220+
(when fast_fail is set to `False`).
213221
"""
214-
resolver, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions)
222+
resolver, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)
215223
global_state = code_generator.global_state
216224
# Do not pass local state so it can recursively call itself.
217225
exec(code_generator.func_code, global_state)
@@ -222,7 +230,7 @@ def compile(definition, handlers={}, formats={}, use_default=True, use_formats=T
222230

223231

224232
# pylint: disable=dangerous-default-value
225-
def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
233+
def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
226234
"""
227235
Generates validation code for validating JSON schema passed in ``definition``.
228236
Example:
@@ -245,15 +253,15 @@ def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_f
245253
Exception :any:`JsonSchemaDefinitionException` is raised when generating the
246254
code fails (bad definition).
247255
"""
248-
_, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions)
256+
_, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)
249257
return (
250258
'VERSION = "' + VERSION + '"\n' +
251259
code_generator.global_state_code + '\n' +
252260
code_generator.func_code
253261
)
254262

255263

256-
def _factory(definition, handlers, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
264+
def _factory(definition, handlers, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
257265
resolver = RefResolver.from_schema(definition, handlers=handlers, store={})
258266
code_generator = _get_code_generator_class(definition)(
259267
definition,
@@ -262,6 +270,7 @@ def _factory(definition, handlers, formats={}, use_default=True, use_formats=Tru
262270
use_default=use_default,
263271
use_formats=use_formats,
264272
detailed_exceptions=detailed_exceptions,
273+
fast_fail=fast_fail,
265274
)
266275
return resolver, code_generator
267276

fastjsonschema/draft04.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ class CodeGeneratorDraft04(CodeGenerator):
3434
'uri': r'^\w+:(\/?\/?)[^\s]+\Z',
3535
}
3636

37-
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
38-
super().__init__(definition, resolver, detailed_exceptions)
37+
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
38+
super().__init__(definition, resolver, detailed_exceptions, fast_fail)
3939
self._custom_formats = formats
4040
self._use_formats = use_formats
4141
self._use_default = use_default

fastjsonschema/draft06.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ class CodeGeneratorDraft06(CodeGeneratorDraft04):
1616
),
1717
})
1818

19-
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
20-
super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions)
19+
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
20+
super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions, fast_fail)
2121
self._json_keywords_to_function.update((
2222
('exclusiveMinimum', self.generate_exclusive_minimum),
2323
('exclusiveMaximum', self.generate_exclusive_maximum),

fastjsonschema/draft07.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class CodeGeneratorDraft07(CodeGeneratorDraft06):
1717
),
1818
})
1919

20-
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
21-
super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions)
20+
def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
21+
super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions, fast_fail)
2222
# pylint: disable=duplicate-code
2323
self._json_keywords_to_function.update((
2424
('if', self.generate_if_then_else),

fastjsonschema/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ def rule_definition(self):
4545
return self.definition.get(self.rule)
4646

4747

48+
class JsonSchemaValuesException(JsonSchemaException):
49+
"""
50+
Exception raised by validation function. It is a collection of all errors.
51+
"""
52+
53+
def __init__(self, errors):
54+
super().__init__()
55+
self.errors = errors
56+
57+
4858
class JsonSchemaDefinitionException(JsonSchemaException):
4959
"""
5060
Exception raised by generator of validation function.

fastjsonschema/generator.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from decimal import Decimal
33
import re
44

5-
from .exceptions import JsonSchemaValueException, JsonSchemaDefinitionException
5+
from .exceptions import JsonSchemaValueException, JsonSchemaValuesException, JsonSchemaDefinitionException
66
from .indent import indent
77
from .ref_resolver import RefResolver
88

@@ -29,11 +29,12 @@ class CodeGenerator:
2929

3030
INDENT = 4 # spaces
3131

32-
def __init__(self, definition, resolver=None, detailed_exceptions=True):
32+
def __init__(self, definition, resolver=None, detailed_exceptions=True, fast_fail=True):
3333
self._code = []
3434
self._compile_regexps = {}
3535
self._custom_formats = {}
3636
self._detailed_exceptions = detailed_exceptions
37+
self._fast_fail = fast_fail
3738

3839
# Any extra library should be here to be imported only once.
3940
# Lines are imports to be printed in the file and objects
@@ -91,6 +92,7 @@ def global_state(self):
9192
REGEX_PATTERNS=self._compile_regexps,
9293
re=re,
9394
JsonSchemaValueException=JsonSchemaValueException,
95+
JsonSchemaValuesException=JsonSchemaValuesException,
9496
)
9597

9698
@property
@@ -103,13 +105,13 @@ def global_state_code(self):
103105

104106
if not self._compile_regexps:
105107
return '\n'.join(self._extra_imports_lines + [
106-
'from fastjsonschema import JsonSchemaValueException',
108+
'from fastjsonschema import JsonSchemaValueException, JsonSchemaValuesException',
107109
'',
108110
'',
109111
])
110112
return '\n'.join(self._extra_imports_lines + [
111113
'import re',
112-
'from fastjsonschema import JsonSchemaValueException',
114+
'from fastjsonschema import JsonSchemaValueException, JsonSchemaValuesException',
113115
'',
114116
'',
115117
'REGEX_PATTERNS = ' + serialize_regexes(self._compile_regexps),
@@ -143,7 +145,11 @@ def generate_validation_function(self, uri, name):
143145
self.l('')
144146
with self._resolver.resolving(uri) as definition:
145147
with self.l('def {}(data, custom_formats={{}}, name_prefix=None):', name):
148+
if not self._fast_fail:
149+
self.l('errors = []')
146150
self.generate_func_code_block(definition, 'data', 'data', clear_variables=True)
151+
if not self._fast_fail:
152+
self.l('if errors: raise JsonSchemaValuesException(errors)')
147153
self.l('return data')
148154

149155
def generate_func_code_block(self, definition, variable, variable_name, clear_variables=False):
@@ -268,13 +274,20 @@ def exc(self, msg, *args, append_to_msg=None, rule=None):
268274
Short-cut for creating raising exception in the code.
269275
"""
270276
if not self._detailed_exceptions:
271-
self.l('raise JsonSchemaValueException("'+msg+'")', *args)
277+
if self._fast_fail:
278+
self.l('raise JsonSchemaValueException("'+msg+'")', *args)
279+
else:
280+
self.l('errors.append(JsonSchemaValueException("'+msg+'"))', *args)
272281
return
273282

274283
arg = '"'+msg+'"'
275284
if append_to_msg:
276285
arg += ' + (' + append_to_msg + ')'
277-
msg = 'raise JsonSchemaValueException('+arg+', value={variable}, name="{name}", definition={definition}, rule={rule})'
286+
msg = (
287+
'raise JsonSchemaValueException('+arg+', value={variable}, name="{name}", definition={definition}, rule={rule})'
288+
if self._fast_fail else
289+
'errors.append(JsonSchemaValueException('+arg+', value={variable}, name="{name}", definition={definition}, rule={rule}))'
290+
)
278291
definition = self._expand_refs(self._definition)
279292
definition_rule = self.e(definition.get(rule) if isinstance(definition, dict) else None)
280293
self.l(msg, *args, definition=repr(definition), rule=repr(rule), definition_rule=definition_rule)

tests/test_fast_fail.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
3+
from fastjsonschema import JsonSchemaValueException, JsonSchemaValuesException, compile
4+
5+
6+
def test_fast_fail():
7+
validator = compile({
8+
'type': 'object',
9+
'properties': {
10+
'string': {
11+
'type': 'string',
12+
},
13+
'number': {
14+
'type': 'number',
15+
},
16+
},
17+
})
18+
19+
with pytest.raises(JsonSchemaValueException) as exc_info:
20+
validator({
21+
'string': 1,
22+
'number': 'a',
23+
})
24+
assert exc_info.value.message == 'data.string must be string'
25+
26+
27+
def test_captures_all_errors():
28+
validator = compile({
29+
'type': 'object',
30+
'properties': {
31+
'string': {
32+
'type': 'string',
33+
},
34+
'number': {
35+
'type': 'number',
36+
},
37+
},
38+
}, fast_fail=False)
39+
40+
with pytest.raises(JsonSchemaValuesException) as exc_info:
41+
validator({
42+
'string': 1,
43+
'number': 'a',
44+
})
45+
assert len(exc_info.value.errors) == 2
46+
assert exc_info.value.errors[0].message == 'data.string must be string'
47+
assert exc_info.value.errors[1].message == 'data.number must be number'

0 commit comments

Comments
 (0)