-
-
Notifications
You must be signed in to change notification settings - Fork 593
Allow exception types to be registered for format functions #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f56c41e
77d64bc
2303c17
3ee6f67
a561d32
2dd9c26
762a507
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,7 @@ class SchemaError(_Error): pass | |
class ValidationError(_Error): pass | ||
class RefResolutionError(Exception): pass | ||
class UnknownType(Exception): pass | ||
class FormatError(Exception): pass | ||
|
||
|
||
def validates(version): | ||
|
@@ -295,10 +296,12 @@ def validate_pattern(self, patrn, instance, schema): | |
def validate_format(self, format, instance, schema): | ||
if ( | ||
self.format_checker is not None and | ||
self.is_type(instance, "string") and | ||
not self.format_checker.conforms(instance, format) | ||
self.is_type(instance, "string") | ||
): | ||
yield ValidationError("%r is not a %r" % (instance, format)) | ||
try: | ||
self.format_checker.conforms(instance, format) | ||
except FormatError as e: | ||
yield ValidationError(unicode(e)) | ||
|
||
def validate_minLength(self, mL, instance, schema): | ||
if self.is_type(instance, "string") and len(instance) < mL: | ||
|
@@ -746,16 +749,18 @@ def __init__(self, formats=None): | |
else: | ||
self.checkers = dict((k, self.checkers[k]) for k in formats) | ||
|
||
def checks(self, format): | ||
def checks(self, format, raises=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe except None as e: which I guess doesn't raise an exception (for trying to catch a thing that could never have been raised) but catching () is at least catching a thing that's valid "input" to except. |
||
""" | ||
Register a decorated function as validating a new format. | ||
|
||
:argument str format: the format that the decorated function will check | ||
:argument raises: Exception that will be caught and used as the | ||
error message | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above re: cross-ref. |
||
|
||
""" | ||
|
||
def _checks(func): | ||
self.checkers[format] = func | ||
self.checkers[format] = (func, raises) | ||
return func | ||
return _checks | ||
|
||
|
@@ -769,11 +774,18 @@ def conforms(self, instance, format): | |
:type: any primitive type (str, number, bool) | ||
:argument str format: the format that instance should conform to | ||
:rtype: bool | ||
:raises: :exc:`FormatError` if instance does not conform to format | ||
|
||
""" | ||
|
||
if format in self.checkers: | ||
return self.checkers[format](instance) | ||
func, raises = self.checkers[format] | ||
try: | ||
result = func(instance) | ||
except raises as e: | ||
raise FormatError(unicode(e)) | ||
if not result: | ||
raise FormatError("%r is not a %r" % (instance, format)) | ||
return True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This return is no longer necessary since essentially someone calling this just cares if it raises an exception or not. I'm not sure how much I like the name |
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ | |
from jsonschema import ( | ||
PY3, SchemaError, UnknownType, ValidationError, ErrorTree, | ||
Draft3Validator, Draft4Validator, FormatChecker, draft3_format_checker, | ||
draft4_format_checker, RefResolver, validate | ||
draft4_format_checker, RefResolver, validate, FormatError | ||
) | ||
|
||
|
||
|
@@ -156,7 +156,7 @@ def test_it_validates_formats_if_a_checker_is_provided(self): | |
|
||
checker.conforms.assert_called_once_with("bar", "foo") | ||
|
||
checker.conforms.return_value = False | ||
checker.conforms.side_effect = FormatError | ||
|
||
with self.assertRaises(ValidationError): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as e: and then outside the block self.assertIs(e.exception.cause, cause) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahhh, yeah I knew there must be a better way. |
||
validator.validate("bar") | ||
|
@@ -338,9 +338,10 @@ def test_additionalProperties_multiple_failures(self): | |
self.assertIn(repr("bar"), message) | ||
self.assertIn("were unexpected)", message) | ||
|
||
def test_invalid_format(self): | ||
checker = mock.Mock(spec=FormatChecker) | ||
checker.conforms.return_value = False | ||
def test_invalid_format_default_message(self): | ||
checker = FormatChecker(formats=()) | ||
check_fn = mock.Mock(return_value=False) | ||
checker.checks("thing")(check_fn) | ||
|
||
schema = {"format" : "thing"} | ||
message = self.message_for("bla", schema, format_checker=checker) | ||
|
@@ -349,6 +350,16 @@ def test_invalid_format(self): | |
self.assertIn(repr("thing"), message) | ||
self.assertIn("is not a", message) | ||
|
||
def test_invalid_format_custom_message(self): | ||
checker = FormatChecker(formats=()) | ||
check_fn = mock.Mock(side_effect=ValueError("custom error")) | ||
checker.checks("thing", raises=ValueError)(check_fn) | ||
|
||
schema = {"format" : "thing"} | ||
message = self.message_for("bla", schema, format_checker=checker) | ||
|
||
self.assertIn("custom error", message) | ||
|
||
|
||
class TestValidationErrorDetails(unittest.TestCase): | ||
def setUp(self): | ||
|
@@ -662,16 +673,29 @@ def test_it_raises_a_key_error_for_unknown_formats(self): | |
def test_it_can_register_cls_checkers(self): | ||
with mock.patch.dict(FormatChecker.checkers, clear=True): | ||
FormatChecker.cls_checks("new")(self.fn) | ||
self.assertEqual(FormatChecker.checkers, {"new" : self.fn}) | ||
self.assertEqual(FormatChecker.checkers, {"new" : (self.fn, None)}) | ||
|
||
def test_it_can_register_checkers(self): | ||
checker = FormatChecker() | ||
checker.checks("new")(self.fn) | ||
self.assertEqual( | ||
checker.checkers, | ||
dict(FormatChecker.checkers, new=self.fn) | ||
dict(FormatChecker.checkers, new=(self.fn, None)) | ||
) | ||
|
||
def test_it_catches_registered_errors(self): | ||
checker = FormatChecker() | ||
checker.checks("foo", raises=ValueError)(self.fn) | ||
# Registered errors should be caught and turned into FormatErrors | ||
self.fn.side_effect = ValueError | ||
with self.assertRaises(FormatError): | ||
checker.conforms("bar", "foo") | ||
# Unregistered errors should not be caught | ||
self.fn.side_effect = AttributeError | ||
with self.assertRaises(AttributeError): | ||
checker.conforms("bar", "foo") | ||
|
||
|
||
|
||
def sorted_errors(errors): | ||
def key(error): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a cross-link here so that people know where these end up? Something like:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.