Skip to content

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

Merged
merged 7 commits into from
Mar 3, 2013
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/validate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,19 +238,22 @@ be enabled by hooking in a format-checking object into an :class:`IValidator`.

.. attribute:: checkers

A mapping of currently known formats to functions that validate them.
New checkers can be added and removed either per-instance or globally
for all checkers using the :meth:`FormatChecker.checks` or
A mapping of currently known formats to tuple of functions that
validate them and errors that should be caught. New checkers can be
added and removed either per-instance or globally for all checkers
using the :meth:`FormatChecker.checks` or
:meth:`FormatChecker.cls_checks` decorators respectively.

.. method:: cls_checks(format)
.. method:: cls_checks(format, raises=None)

Register a decorated function as *globally* validating a new format.

Any instance created after this function is called will pick up the
supplied checker.

:argument str format: the format that the decorated function will check
:argument raises: Exception that will be caught and used as the
Copy link
Member

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:

:argument Exception raises: the exception(s) raised by the decorated function when an invalid instance is found. The exception object will be accessible as the :attr:`ValidationError.cause` attribute of the resulting validation error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

error message



Expand Down
24 changes: 18 additions & 6 deletions jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe () is a better default? As-is, with no raises we'll do

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
Copy link
Member

Choose a reason for hiding this comment

The 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

Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 conforms for this anymore. conforms sounds like it should return a bool. That's why I suggested check do this, and leave conforms to return a bool if check raised a FormatError (even though in our code we'd never need to use conforms anymore).



Expand Down
38 changes: 31 additions & 7 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The 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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down