From 3c6568b2171794aadc8df25dd79a5d491008fb48 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 22 Sep 2013 12:32:33 -0400 Subject: [PATCH 1/8] Sort errors based on their paths. --- jsonschema/exceptions.py | 50 ++++++++++++++++++----------- jsonschema/tests/test_exceptions.py | 40 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 jsonschema/tests/test_exceptions.py diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index e94907ddc..fe592e273 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -24,24 +24,19 @@ def __init__( self.instance = instance self.schema = schema - @classmethod - def create_from(cls, other): - return cls( - message=other.message, - cause=other.cause, - context=other.context, - path=other.path, - schema_path=other.schema_path, - validator=other.validator, - validator_value=other.validator_value, - instance=other.instance, - schema=other.schema, - ) - - def _set(self, **kwargs): - for k, v in iteritems(kwargs): - if getattr(self, k) is _unset: - setattr(self, k, v) + def __lt__(self, other): + if not isinstance(other, self.__class__): + # On Py2 Python will "helpfully" make this succeed. So be more + # forceful, because we really don't want this to work, it probably + # means a ValidationError and a SchemaError are being compared + # accidentally. + if not PY3: + message = "unorderable types: %s() < %s()" % ( + self.__class__.__name__, other.__class__.__name__, + ) + raise TypeError(message) + return NotImplemented + return self.path < other.path def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.message) @@ -79,6 +74,25 @@ def __unicode__(self): if PY3: __str__ = __unicode__ + @classmethod + def create_from(cls, other): + return cls( + message=other.message, + cause=other.cause, + context=other.context, + path=other.path, + schema_path=other.schema_path, + validator=other.validator, + validator_value=other.validator_value, + instance=other.instance, + schema=other.schema, + ) + + def _set(self, **kwargs): + for k, v in iteritems(kwargs): + if getattr(self, k) is _unset: + setattr(self, k, v) + class ValidationError(_Error): pass diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py new file mode 100644 index 000000000..b7ecaae2d --- /dev/null +++ b/jsonschema/tests/test_exceptions.py @@ -0,0 +1,40 @@ +from jsonschema import Draft4Validator, exceptions +from jsonschema.tests.compat import mock, unittest + + +class TestValidationErrorSorting(unittest.TestCase): + def test_shallower_errors_are_better_matches(self): + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "minProperties" : 2, + "properties" : {"bar" : {"type" : "object"}}, + } + } + } + ) + errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], ["foo", "bar"]], + ) + + def test_global_errors_are_even_better_matches(self): + validator = Draft4Validator( + { + "minProperties" : 2, + "properties" : {"foo" : {"type" : "array"}}, + } + ) + errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) + self.assertEqual( + [list(error.path) for error in errors], + [[], ["foo"]], + ) + + def test_cannot_sort_errors_of_mixed_types(self): + with self.assertRaises(TypeError): + v = exceptions.ValidationError("Oh", instance=3) + s = exceptions.SchemaError("No!", instance=3) + v < s From 8199901c3b434366edb8b87d5b56c65ca624bcca Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 22 Sep 2013 18:37:45 -0400 Subject: [PATCH 2/8] Initial stab at best_match. --- docs/errors.rst | 31 ++++++ jsonschema/exceptions.py | 33 +++++- jsonschema/tests/test_exceptions.py | 152 +++++++++++++++++++++++++++- 3 files changed, 213 insertions(+), 3 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 9f63c25d9..c3cf096d7 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -301,3 +301,34 @@ To summarize, each tree contains child trees that can be accessed by indexing the tree to get the corresponding child tree for a given index into the instance. Each tree and child has a :attr:`~ErrorTree.errors` attribute, a dict, that maps the failed validator to the corresponding validation error. + + +best_match +---------- + +The :func:`best_match` function is a simple but useful function for attempting +to guess the most relevant error in a given bunch. + +.. autofunction:: best_match + + Try to find an error that appears to be the best match among given errors. + + In general, errors that are higher up in the instance (i.e. for which + :attr:`ValidationError.path` is shorter) are considered better matches, + since they indicate "more" is wrong with the instance. + + If the resulting match is either :validator:`oneOf` or :validator:`anyOf`, + the *opposite* assumption is made -- i.e. the deepest error is picked, + since these validators only need to match once, and any other errors may + not be relevant. + + :argument iterable errors: the errors to select from. Do not provide a + mixture of errors from different validation attempts (i.e. from + different instances or schemas), since it won't produce sensical + output. + :returns: the best matching error, or ``None`` if the iterable was empty + + .. note:: + + This function is a heuristic. Its return value may change for a given + set of inputs from version to version if better heuristics are added. diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index fe592e273..5c52f41b2 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -1,4 +1,5 @@ import collections +import itertools import pprint import textwrap @@ -24,6 +25,11 @@ def __init__( self.instance = instance self.schema = schema + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self._contents() == other._contents() + def __lt__(self, other): if not isinstance(other, self.__class__): # On Py2 Python will "helpfully" make this succeed. So be more @@ -36,7 +42,14 @@ def __lt__(self, other): ) raise TypeError(message) return NotImplemented - return self.path < other.path + + is_deeper = len(self.path) > len(other.path) + is_weak_matcher = self.validator in ("anyOf", "oneOf") + other_is_weak_matcher = other.validator in ("anyOf", "oneOf") + return is_deeper or is_weak_matcher > other_is_weak_matcher + + def __ne__(self, other): + return not self == other def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.message) @@ -93,6 +106,14 @@ def _set(self, **kwargs): if getattr(self, k) is _unset: setattr(self, k, v) + def _contents(self): + return dict( + (attr, getattr(self, attr)) for attr in ( + "message", "cause", "context", "path", "schema_path", + "validator", "validator_value", "instance", "schema" + ) + ) + class ValidationError(_Error): pass @@ -146,3 +167,13 @@ def __unicode__(self): if PY3: __str__ = __unicode__ + + +def best_match(errors): + first = next(iter(errors), None) + if first is None: + return + best = max(itertools.chain([first], errors)) + while best.context: + best = min(best.context) + return best diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index b7ecaae2d..92976b37f 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -17,7 +17,7 @@ def test_shallower_errors_are_better_matches(self): errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) self.assertEqual( [list(error.path) for error in errors], - [["foo"], ["foo", "bar"]], + [["foo", "bar"], ["foo"]], ) def test_global_errors_are_even_better_matches(self): @@ -30,7 +30,25 @@ def test_global_errors_are_even_better_matches(self): errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) self.assertEqual( [list(error.path) for error in errors], - [[], ["foo"]], + [["foo"], []], + ) + + def test_oneOf_and_anyOf_are_weak_matches(self): + """ + A property you *must* match is probably better than one you have to + match a part of. + + """ + + validator = Draft4Validator( + { + "minProperties" : 2, + "oneOf" : [{"type" : "string"}, {"type" : "number"}], + } + ) + errors = sorted(validator.iter_errors({})) + self.assertEqual( + [error.validator for error in errors], ["oneOf", "minProperties"], ) def test_cannot_sort_errors_of_mixed_types(self): @@ -38,3 +56,133 @@ def test_cannot_sort_errors_of_mixed_types(self): v = exceptions.ValidationError("Oh", instance=3) s = exceptions.SchemaError("No!", instance=3) v < s + + +class TestBestMatch(unittest.TestCase): + def test_for_errors_without_context_it_returns_the_max(self): + """ + The ``max`` will be the error which is most "shallow" in the instance. + + """ + + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "minProperties" : 2, + "properties" : {"bar" : {"type" : "object"}}, + }, + }, + } + ) + errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) + self.assertIs(exceptions.best_match(errors), errors[-1]) + + def test_context_for_anyOf(self): + """ + For the anyOf validator, we use the min, to assume the least. + + Other errors are not necessarily relevant, since only one needs to + match. + + """ + + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "anyOf" : [ + {"type" : "string"}, + {"properties" : {"bar" : {"type" : "array"}}}, + ], + }, + }, + }, + ) + errors = validator.iter_errors({"foo" : {"bar" : 12}}) + best = exceptions.best_match(errors) + self.assertEqual(best.validator_value, "array") + + def test_context_for_oneOf(self): + """ + For the oneOf validator, we use the min, to assume the least. + + Other errors are not necessarily relevant, since only one needs to + match. + + """ + + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "oneOf" : [ + {"type" : "string"}, + {"properties" : {"bar" : {"type" : "array"}}}, + ], + }, + }, + }, + ) + errors = validator.iter_errors({"foo" : {"bar" : 12}}) + best = exceptions.best_match(errors) + self.assertEqual(best.validator_value, "array") + + def test_context_for_allOf(self): + """ + allOf just yields all the errors globally, so each should be considered + + """ + + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "allOf" : [ + {"type" : "string"}, + {"properties" : {"bar" : {"type" : "array"}}}, + ], + }, + }, + }, + ) + errors = validator.iter_errors({"foo" : {"bar" : 12}}) + best = exceptions.best_match(errors) + self.assertEqual(best.validator_value, "string") + + def test_nested_context_for_oneOf(self): + validator = Draft4Validator( + { + "properties" : { + "foo" : { + "oneOf" : [ + {"type" : "string"}, + { + "oneOf" : [ + {"type" : "string"}, + { + "properties" : { + "bar" : {"type" : "array"} + }, + }, + ], + }, + ], + }, + }, + }, + ) + errors = validator.iter_errors({"foo" : {"bar" : 12}}) + best = exceptions.best_match(errors) + self.assertEqual(best.validator_value, "array") + + def test_one_error(self): + validator = Draft4Validator({"minProperties" : 2}) + error, = validator.iter_errors({}) + self.assertEqual( + exceptions.best_match(validator.iter_errors({})), error, + ) + + def test_no_errors(self): + validator = Draft4Validator({}) + self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) From 11b3220eac39292e88df04a8b8a5a142812496fe Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 22 Sep 2013 18:44:33 -0400 Subject: [PATCH 3/8] Use ._contents in create_from --- jsonschema/exceptions.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 5c52f41b2..8df987d8a 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -89,17 +89,7 @@ def __unicode__(self): @classmethod def create_from(cls, other): - return cls( - message=other.message, - cause=other.cause, - context=other.context, - path=other.path, - schema_path=other.schema_path, - validator=other.validator, - validator_value=other.validator_value, - instance=other.instance, - schema=other.schema, - ) + return cls(**other._contents()) def _set(self, **kwargs): for k, v in iteritems(kwargs): From b4f098bdccb25b6d124050e4ecda48371c7ccc4a Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 27 Oct 2013 20:38:49 -0400 Subject: [PATCH 4/8] Different strategy that's a lot more robust. --- jsonschema/exceptions.py | 40 +++---- jsonschema/tests/test_exceptions.py | 157 ++++++++++++++++------------ 2 files changed, 106 insertions(+), 91 deletions(-) diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 8df987d8a..04a3c0b17 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -7,6 +7,9 @@ from jsonschema.compat import PY3, iteritems +WEAK_MATCHES = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES = frozenset() + _unset = _utils.Unset() @@ -30,24 +33,6 @@ def __eq__(self, other): return NotImplemented return self._contents() == other._contents() - def __lt__(self, other): - if not isinstance(other, self.__class__): - # On Py2 Python will "helpfully" make this succeed. So be more - # forceful, because we really don't want this to work, it probably - # means a ValidationError and a SchemaError are being compared - # accidentally. - if not PY3: - message = "unorderable types: %s() < %s()" % ( - self.__class__.__name__, other.__class__.__name__, - ) - raise TypeError(message) - return NotImplemented - - is_deeper = len(self.path) > len(other.path) - is_weak_matcher = self.validator in ("anyOf", "oneOf") - other_is_weak_matcher = other.validator in ("anyOf", "oneOf") - return is_deeper or is_weak_matcher > other_is_weak_matcher - def __ne__(self, other): return not self == other @@ -159,11 +144,20 @@ def __unicode__(self): __str__ = __unicode__ -def best_match(errors): - first = next(iter(errors), None) - if first is None: +def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): + def relevance(error): + validator = error.validator + return -len(error.path), validator not in weak, validator in strong + return relevance + + +def best_match(errors, key=by_relevance()): + errors = iter(errors) + best = next(errors, None) + if best is None: return - best = max(itertools.chain([first], errors)) + best = max(itertools.chain([best], errors), key=key) + while best.context: - best = min(best.context) + best = min(best.context, key=key) return best diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index 92976b37f..d6c7490c8 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -2,7 +2,19 @@ from jsonschema.tests.compat import mock, unittest -class TestValidationErrorSorting(unittest.TestCase): +class TestBestMatch(unittest.TestCase): + def best_match(self, errors): + errors = list(errors) + best = exceptions.best_match(errors) + reversed_best = exceptions.best_match(reversed(errors)) + self.assertEqual( + best, + reversed_best, + msg="Didn't return a consistent best match!\n" + "Got: {0}\n\nThen: {1}".format(best, reversed_best), + ) + return best + def test_shallower_errors_are_better_matches(self): validator = Draft4Validator( { @@ -14,24 +26,8 @@ def test_shallower_errors_are_better_matches(self): } } ) - errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) - self.assertEqual( - [list(error.path) for error in errors], - [["foo", "bar"], ["foo"]], - ) - - def test_global_errors_are_even_better_matches(self): - validator = Draft4Validator( - { - "minProperties" : 2, - "properties" : {"foo" : {"type" : "array"}}, - } - ) - errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) - self.assertEqual( - [list(error.path) for error in errors], - [["foo"], []], - ) + best = self.best_match(validator.iter_errors({"foo" : {"bar" : []}})) + self.assertEqual(best.validator, "minProperties") def test_oneOf_and_anyOf_are_weak_matches(self): """ @@ -43,47 +39,21 @@ def test_oneOf_and_anyOf_are_weak_matches(self): validator = Draft4Validator( { "minProperties" : 2, + "anyOf" : [{"type" : "string"}, {"type" : "number"}], "oneOf" : [{"type" : "string"}, {"type" : "number"}], } ) - errors = sorted(validator.iter_errors({})) - self.assertEqual( - [error.validator for error in errors], ["oneOf", "minProperties"], - ) - - def test_cannot_sort_errors_of_mixed_types(self): - with self.assertRaises(TypeError): - v = exceptions.ValidationError("Oh", instance=3) - s = exceptions.SchemaError("No!", instance=3) - v < s - - -class TestBestMatch(unittest.TestCase): - def test_for_errors_without_context_it_returns_the_max(self): - """ - The ``max`` will be the error which is most "shallow" in the instance. - - """ - - validator = Draft4Validator( - { - "properties" : { - "foo" : { - "minProperties" : 2, - "properties" : {"bar" : {"type" : "object"}}, - }, - }, - } - ) - errors = sorted(validator.iter_errors({"foo" : {"bar" : []}})) - self.assertIs(exceptions.best_match(errors), errors[-1]) + best = self.best_match(validator.iter_errors({})) + self.assertEqual(best.validator, "minProperties") - def test_context_for_anyOf(self): + def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): """ - For the anyOf validator, we use the min, to assume the least. + If the most relevant error is an anyOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. - Other errors are not necessarily relevant, since only one needs to - match. + I.e. since only one of the schemas must match, we look for the most + relevant one. """ @@ -99,16 +69,17 @@ def test_context_for_anyOf(self): }, }, ) - errors = validator.iter_errors({"foo" : {"bar" : 12}}) - best = exceptions.best_match(errors) + best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) self.assertEqual(best.validator_value, "array") - def test_context_for_oneOf(self): + def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): """ - For the oneOf validator, we use the min, to assume the least. + If the most relevant error is an oneOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. - Other errors are not necessarily relevant, since only one needs to - match. + I.e. since only one of the schemas must match, we look for the most + relevant one. """ @@ -124,13 +95,13 @@ def test_context_for_oneOf(self): }, }, ) - errors = validator.iter_errors({"foo" : {"bar" : 12}}) - best = exceptions.best_match(errors) + best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) self.assertEqual(best.validator_value, "array") - def test_context_for_allOf(self): + def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): """ - allOf just yields all the errors globally, so each should be considered + Now, if the error is allOf, we traverse but select the *most* relevant + error from the context, because all schemas here must match anyways. """ @@ -146,8 +117,7 @@ def test_context_for_allOf(self): }, }, ) - errors = validator.iter_errors({"foo" : {"bar" : 12}}) - best = exceptions.best_match(errors) + best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) self.assertEqual(best.validator_value, "string") def test_nested_context_for_oneOf(self): @@ -172,8 +142,7 @@ def test_nested_context_for_oneOf(self): }, }, ) - errors = validator.iter_errors({"foo" : {"bar" : 12}}) - best = exceptions.best_match(errors) + best = self.best_match(validator.iter_errors({"foo" : {"bar" : 12}})) self.assertEqual(best.validator_value, "array") def test_one_error(self): @@ -186,3 +155,55 @@ def test_one_error(self): def test_no_errors(self): validator = Draft4Validator({}) self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) + + +class TestByRelevance(unittest.TestCase): + def test_short_paths_are_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=["baz"]) + deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) + match = max([shallow, deep], key=exceptions.by_relevance()) + self.assertIs(match, shallow) + + match = max([deep, shallow], key=exceptions.by_relevance()) + self.assertIs(match, shallow) + + def test_global_errors_are_even_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=[]) + deep = exceptions.ValidationError("Oh yes!", path=["foo"]) + + errors = sorted([shallow, deep], key=exceptions.by_relevance()) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + errors = sorted([deep, shallow], key=exceptions.by_relevance()) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + def test_weak_validators_are_lower_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + + best_match = exceptions.by_relevance(weak="a") + + match = max([weak, normal], key=best_match) + self.assertIs(match, normal) + + match = max([normal, weak], key=best_match) + self.assertIs(match, normal) + + def test_strong_validators_are_higher_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") + + best_match = exceptions.by_relevance(weak="a", strong="c") + + match = max([weak, normal, strong], key=best_match) + self.assertIs(match, strong) + + match = max([strong, normal, weak], key=best_match) + self.assertIs(match, strong) From b805101de629f96fbbec455a1d20b46a1fdb9605 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 27 Oct 2013 21:05:52 -0400 Subject: [PATCH 5/8] Update best_match docs. --- docs/errors.rst | 22 ++++++++++++++++++++-- jsonschema/tests/test_exceptions.py | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index c3cf096d7..3e19ea97d 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -2,7 +2,7 @@ Handling Validation Errors ========================== -.. currentmodule:: jsonschema +.. currentmodule:: jsonschema.exceptions When an invalid instance is encountered, a :exc:`ValidationError` will be raised or returned, depending on which method or function is used. @@ -194,7 +194,7 @@ If you want to programmatically be able to query which properties or validators failed when validating a given instance, you probably will want to do so using :class:`ErrorTree` objects. -.. autoclass:: ErrorTree +.. autoclass:: jsonschema.validators.ErrorTree :members: :special-members: :exclude-members: __dict__,__weakref__ @@ -317,6 +317,18 @@ to guess the most relevant error in a given bunch. :attr:`ValidationError.path` is shorter) are considered better matches, since they indicate "more" is wrong with the instance. +.. doctest:: + + >>> from jsonschema import Draft4Validator + >>> from jsonschema.exceptions import best_match + + >>> schema = { + ... "type": "array", + ... "minItems": 3, + ... } + >>> print(best_match(Draft4Validator(schema).iter_errors(11)).message) + 11 is not of type 'array' + If the resulting match is either :validator:`oneOf` or :validator:`anyOf`, the *opposite* assumption is made -- i.e. the deepest error is picked, since these validators only need to match once, and any other errors may @@ -326,9 +338,15 @@ to guess the most relevant error in a given bunch. mixture of errors from different validation attempts (i.e. from different instances or schemas), since it won't produce sensical output. + :argument callable key: the key to use when sorting errors. See + :func:`by_relevance` for more details (the default is to sort with the + defaults of that function). :returns: the best matching error, or ``None`` if the iterable was empty .. note:: This function is a heuristic. Its return value may change for a given set of inputs from version to version if better heuristics are added. + + +.. autofunction:: best_match diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index d6c7490c8..b3f176d70 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -1,5 +1,5 @@ from jsonschema import Draft4Validator, exceptions -from jsonschema.tests.compat import mock, unittest +from jsonschema.tests.compat import unittest class TestBestMatch(unittest.TestCase): From 6b8e1f4bcc5ae851abea99441815382d43e2b243 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 27 Oct 2013 21:11:28 -0400 Subject: [PATCH 6/8] Remove __eq__, since it causes hashability issues on Py3 that I don't want to deal with at the moment. --- jsonschema/exceptions.py | 8 -------- jsonschema/tests/test_exceptions.py | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 04a3c0b17..26fafbe25 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -28,14 +28,6 @@ def __init__( self.instance = instance self.schema = schema - def __eq__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - return self._contents() == other._contents() - - def __ne__(self, other): - return not self == other - def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.message) diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index b3f176d70..2014a6490 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -149,7 +149,8 @@ def test_one_error(self): validator = Draft4Validator({"minProperties" : 2}) error, = validator.iter_errors({}) self.assertEqual( - exceptions.best_match(validator.iter_errors({})), error, + exceptions.best_match(validator.iter_errors({})).validator, + "minProperties", ) def test_no_errors(self): From 4f9447b05b80b6965e31eeb7dd2f4aa58bc78ebb Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 27 Oct 2013 21:26:00 -0400 Subject: [PATCH 7/8] And add by_relevance docs. --- docs/errors.rst | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 3e19ea97d..14b21b956 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -303,8 +303,8 @@ instance. Each tree and child has a :attr:`~ErrorTree.errors` attribute, a dict, that maps the failed validator to the corresponding validation error. -best_match ----------- +best_match and by_relevance +--------------------------- The :func:`best_match` function is a simple but useful function for attempting to guess the most relevant error in a given bunch. @@ -349,4 +349,31 @@ to guess the most relevant error in a given bunch. set of inputs from version to version if better heuristics are added. -.. autofunction:: best_match +.. autofunction:: by_relevance + + Create a key function that can be used to sort errors by relevance. + + If you want to sort a bunch of errors entirely, you can use this function + to do so. Using the return value of this function as a key to e.g. + :func:`sorted` or :func:`max` will cause more relevant errors to be + considered greater than less relevant ones. + +.. doctest:: + + >>> schema = { + ... "properties": { + ... "name": {"type": "string"}, + ... "phones": { + ... "properties": { + ... "home": {"type": "string"} + ... }, + ... }, + ... }, + ... } + >>> instance = {"name": 123, "phones": {"home": [123]}} + >>> errors = Draft4Validator(schema).iter_errors(instance) + >>> [ + ... e.path[-1] + ... for e in sorted(errors, key=exceptions.by_relevance()) + ... ] + ['home', 'name'] From 4f171aa18393682f2a0a3a6a178717eda021dd2c Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 27 Oct 2013 21:28:09 -0400 Subject: [PATCH 8/8] And docs for the arguments. --- docs/errors.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/errors.rst b/docs/errors.rst index 14b21b956..7fbd2f03d 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -377,3 +377,11 @@ to guess the most relevant error in a given bunch. ... for e in sorted(errors, key=exceptions.by_relevance()) ... ] ['home', 'name'] + + :argument set weak: a collection of validators to consider to be "weak". If + there are two errors at the same level of the instance and one is in + the set of weak validators, the other error will take priority. By + default, :validator:`anyOf` and :validator:`oneOf` are considered weak + validators and will be superceded by other same-level validation + errors. + :argument set strong a collection of validators to consider to be "strong".