123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- from unittest import TestCase
- import textwrap
- from jsonschema import exceptions
- from jsonschema.validators import _LATEST_VERSION
- class TestBestMatch(TestCase):
- def best_match_of(self, instance, schema):
- errors = list(_LATEST_VERSION(schema).iter_errors(instance))
- msg = f"No errors found for {instance} under {schema!r}!"
- self.assertTrue(errors, msg=msg)
- best = exceptions.best_match(iter(errors))
- reversed_best = exceptions.best_match(reversed(errors))
- self.assertEqual(
- best._contents(),
- reversed_best._contents(),
- f"No consistent best match!\nGot: {best}\n\nThen: {reversed_best}",
- )
- return best
- def test_shallower_errors_are_better_matches(self):
- schema = {
- "properties": {
- "foo": {
- "minProperties": 2,
- "properties": {"bar": {"type": "object"}},
- },
- },
- }
- best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
- self.assertEqual(best.validator, "minProperties")
- 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.
- """
- schema = {
- "minProperties": 2,
- "anyOf": [{"type": "string"}, {"type": "number"}],
- "oneOf": [{"type": "string"}, {"type": "number"}],
- }
- best = self.best_match_of(instance={}, schema=schema)
- self.assertEqual(best.validator, "minProperties")
- def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self):
- """
- 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.
- I.e. since only one of the schemas must match, we look for the most
- relevant one.
- """
- schema = {
- "properties": {
- "foo": {
- "anyOf": [
- {"type": "string"},
- {"properties": {"bar": {"type": "array"}}},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
- self.assertEqual(best.validator_value, "array")
- def test_no_anyOf_traversal_for_equally_relevant_errors(self):
- """
- We don't traverse into an anyOf (as above) if all of its context errors
- seem to be equally "wrong" against the instance.
- """
- schema = {
- "anyOf": [
- {"type": "string"},
- {"type": "integer"},
- {"type": "object"},
- ],
- }
- best = self.best_match_of(instance=[], schema=schema)
- self.assertEqual(best.validator, "anyOf")
- def test_anyOf_traversal_for_single_equally_relevant_error(self):
- """
- We *do* traverse anyOf with a single nested error, even though it is
- vacuously equally relevant to itself.
- """
- schema = {
- "anyOf": [
- {"type": "string"},
- ],
- }
- best = self.best_match_of(instance=[], schema=schema)
- self.assertEqual(best.validator, "type")
- def test_anyOf_traversal_for_single_sibling_errors(self):
- """
- We *do* traverse anyOf with a single subschema that fails multiple
- times (e.g. on multiple items).
- """
- schema = {
- "anyOf": [
- {"items": {"const": 37}},
- ],
- }
- best = self.best_match_of(instance=[12, 12], schema=schema)
- self.assertEqual(best.validator, "const")
- def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):
- """
- We *do* traverse anyOf with multiple subschemas when one does not type
- match.
- """
- schema = {
- "anyOf": [
- {"type": "object"},
- {"items": {"const": 37}},
- ],
- }
- best = self.best_match_of(instance=[12, 12], schema=schema)
- self.assertEqual(best.validator, "const")
- def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
- """
- 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.
- I.e. since only one of the schemas must match, we look for the most
- relevant one.
- """
- schema = {
- "properties": {
- "foo": {
- "oneOf": [
- {"type": "string"},
- {"properties": {"bar": {"type": "array"}}},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
- self.assertEqual(best.validator_value, "array")
- def test_no_oneOf_traversal_for_equally_relevant_errors(self):
- """
- We don't traverse into an oneOf (as above) if all of its context errors
- seem to be equally "wrong" against the instance.
- """
- schema = {
- "oneOf": [
- {"type": "string"},
- {"type": "integer"},
- {"type": "object"},
- ],
- }
- best = self.best_match_of(instance=[], schema=schema)
- self.assertEqual(best.validator, "oneOf")
- def test_oneOf_traversal_for_single_equally_relevant_error(self):
- """
- We *do* traverse oneOf with a single nested error, even though it is
- vacuously equally relevant to itself.
- """
- schema = {
- "oneOf": [
- {"type": "string"},
- ],
- }
- best = self.best_match_of(instance=[], schema=schema)
- self.assertEqual(best.validator, "type")
- def test_oneOf_traversal_for_single_sibling_errors(self):
- """
- We *do* traverse oneOf with a single subschema that fails multiple
- times (e.g. on multiple items).
- """
- schema = {
- "oneOf": [
- {"items": {"const": 37}},
- ],
- }
- best = self.best_match_of(instance=[12, 12], schema=schema)
- self.assertEqual(best.validator, "const")
- def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):
- """
- We *do* traverse oneOf with multiple subschemas when one does not type
- match.
- """
- schema = {
- "oneOf": [
- {"type": "object"},
- {"items": {"const": 37}},
- ],
- }
- best = self.best_match_of(instance=[12, 12], schema=schema)
- self.assertEqual(best.validator, "const")
- def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
- """
- Now, if the error is allOf, we traverse but select the *most* relevant
- error from the context, because all schemas here must match anyways.
- """
- schema = {
- "properties": {
- "foo": {
- "allOf": [
- {"type": "string"},
- {"properties": {"bar": {"type": "array"}}},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
- self.assertEqual(best.validator_value, "string")
- def test_nested_context_for_oneOf(self):
- """
- We traverse into nested contexts (a oneOf containing an error in a
- nested oneOf here).
- """
- schema = {
- "properties": {
- "foo": {
- "oneOf": [
- {"type": "string"},
- {
- "oneOf": [
- {"type": "string"},
- {
- "properties": {
- "bar": {"type": "array"},
- },
- },
- ],
- },
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
- self.assertEqual(best.validator_value, "array")
- def test_it_prioritizes_matching_types(self):
- schema = {
- "properties": {
- "foo": {
- "anyOf": [
- {"type": "array", "minItems": 2},
- {"type": "string", "minLength": 10},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
- self.assertEqual(best.validator, "minLength")
- reordered = {
- "properties": {
- "foo": {
- "anyOf": [
- {"type": "string", "minLength": 10},
- {"type": "array", "minItems": 2},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)
- self.assertEqual(best.validator, "minLength")
- def test_it_prioritizes_matching_union_types(self):
- schema = {
- "properties": {
- "foo": {
- "anyOf": [
- {"type": ["array", "object"], "minItems": 2},
- {"type": ["integer", "string"], "minLength": 10},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
- self.assertEqual(best.validator, "minLength")
- reordered = {
- "properties": {
- "foo": {
- "anyOf": [
- {"type": "string", "minLength": 10},
- {"type": "array", "minItems": 2},
- ],
- },
- },
- }
- best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)
- self.assertEqual(best.validator, "minLength")
- def test_boolean_schemas(self):
- schema = {"properties": {"foo": False}}
- best = self.best_match_of(instance={"foo": "bar"}, schema=schema)
- self.assertIsNone(best.validator)
- def test_one_error(self):
- validator = _LATEST_VERSION({"minProperties": 2})
- error, = validator.iter_errors({})
- self.assertEqual(
- exceptions.best_match(validator.iter_errors({})).validator,
- "minProperties",
- )
- def test_no_errors(self):
- validator = _LATEST_VERSION({})
- self.assertIsNone(exceptions.best_match(validator.iter_errors({})))
- class TestByRelevance(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.relevance)
- self.assertIs(match, shallow)
- match = max([deep, shallow], key=exceptions.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.relevance)
- self.assertEqual(
- [list(error.path) for error in errors],
- [["foo"], []],
- )
- errors = sorted([deep, shallow], key=exceptions.relevance)
- self.assertEqual(
- [list(error.path) for error in errors],
- [["foo"], []],
- )
- def test_weak_keywords_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_keywords_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)
- class TestErrorTree(TestCase):
- def test_it_knows_how_many_total_errors_it_contains(self):
- # FIXME: #442
- errors = [
- exceptions.ValidationError("Something", validator=i)
- for i in range(8)
- ]
- tree = exceptions.ErrorTree(errors)
- self.assertEqual(tree.total_errors, 8)
- def test_it_contains_an_item_if_the_item_had_an_error(self):
- errors = [exceptions.ValidationError("a message", path=["bar"])]
- tree = exceptions.ErrorTree(errors)
- self.assertIn("bar", tree)
- def test_it_does_not_contain_an_item_if_the_item_had_no_error(self):
- errors = [exceptions.ValidationError("a message", path=["bar"])]
- tree = exceptions.ErrorTree(errors)
- self.assertNotIn("foo", tree)
- def test_keywords_that_failed_appear_in_errors_dict(self):
- error = exceptions.ValidationError("a message", validator="foo")
- tree = exceptions.ErrorTree([error])
- self.assertEqual(tree.errors, {"foo": error})
- def test_it_creates_a_child_tree_for_each_nested_path(self):
- errors = [
- exceptions.ValidationError("a bar message", path=["bar"]),
- exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]),
- ]
- tree = exceptions.ErrorTree(errors)
- self.assertIn(0, tree["bar"])
- self.assertNotIn(1, tree["bar"])
- def test_children_have_their_errors_dicts_built(self):
- e1, e2 = (
- exceptions.ValidationError("1", validator="foo", path=["bar", 0]),
- exceptions.ValidationError("2", validator="quux", path=["bar", 0]),
- )
- tree = exceptions.ErrorTree([e1, e2])
- self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2})
- def test_multiple_errors_with_instance(self):
- e1, e2 = (
- exceptions.ValidationError(
- "1",
- validator="foo",
- path=["bar", "bar2"],
- instance="i1"),
- exceptions.ValidationError(
- "2",
- validator="quux",
- path=["foobar", 2],
- instance="i2"),
- )
- exceptions.ErrorTree([e1, e2])
- def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self):
- error = exceptions.ValidationError("123", validator="foo", instance=[])
- tree = exceptions.ErrorTree([error])
- with self.assertRaises(IndexError):
- tree[0]
- def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self):
- """
- If a keyword refers to a path that isn't in the instance, the
- tree still properly returns a subtree for that path.
- """
- error = exceptions.ValidationError(
- "a message", validator="foo", instance={}, path=["foo"],
- )
- tree = exceptions.ErrorTree([error])
- self.assertIsInstance(tree["foo"], exceptions.ErrorTree)
- def test_iter(self):
- e1, e2 = (
- exceptions.ValidationError(
- "1",
- validator="foo",
- path=["bar", "bar2"],
- instance="i1"),
- exceptions.ValidationError(
- "2",
- validator="quux",
- path=["foobar", 2],
- instance="i2"),
- )
- tree = exceptions.ErrorTree([e1, e2])
- self.assertEqual(set(tree), {"bar", "foobar"})
- def test_repr_single(self):
- error = exceptions.ValidationError(
- "1",
- validator="foo",
- path=["bar", "bar2"],
- instance="i1",
- )
- tree = exceptions.ErrorTree([error])
- self.assertEqual(repr(tree), "<ErrorTree (1 total error)>")
- def test_repr_multiple(self):
- e1, e2 = (
- exceptions.ValidationError(
- "1",
- validator="foo",
- path=["bar", "bar2"],
- instance="i1"),
- exceptions.ValidationError(
- "2",
- validator="quux",
- path=["foobar", 2],
- instance="i2"),
- )
- tree = exceptions.ErrorTree([e1, e2])
- self.assertEqual(repr(tree), "<ErrorTree (2 total errors)>")
- def test_repr_empty(self):
- tree = exceptions.ErrorTree([])
- self.assertEqual(repr(tree), "<ErrorTree (0 total errors)>")
- class TestErrorInitReprStr(TestCase):
- def make_error(self, **kwargs):
- defaults = dict(
- message="hello",
- validator="type",
- validator_value="string",
- instance=5,
- schema={"type": "string"},
- )
- defaults.update(kwargs)
- return exceptions.ValidationError(**defaults)
- def assertShows(self, expected, **kwargs):
- expected = textwrap.dedent(expected).rstrip("\n")
- error = self.make_error(**kwargs)
- message_line, _, rest = str(error).partition("\n")
- self.assertEqual(message_line, error.message)
- self.assertEqual(rest, expected)
- def test_it_calls_super_and_sets_args(self):
- error = self.make_error()
- self.assertGreater(len(error.args), 1)
- def test_repr(self):
- self.assertEqual(
- repr(exceptions.ValidationError(message="Hello!")),
- "<ValidationError: 'Hello!'>",
- )
- def test_unset_error(self):
- error = exceptions.ValidationError("message")
- self.assertEqual(str(error), "message")
- kwargs = {
- "validator": "type",
- "validator_value": "string",
- "instance": 5,
- "schema": {"type": "string"},
- }
- # Just the message should show if any of the attributes are unset
- for attr in kwargs:
- k = dict(kwargs)
- del k[attr]
- error = exceptions.ValidationError("message", **k)
- self.assertEqual(str(error), "message")
- def test_empty_paths(self):
- self.assertShows(
- """
- Failed validating 'type' in schema:
- {'type': 'string'}
- On instance:
- 5
- """,
- path=[],
- schema_path=[],
- )
- def test_one_item_paths(self):
- self.assertShows(
- """
- Failed validating 'type' in schema:
- {'type': 'string'}
- On instance[0]:
- 5
- """,
- path=[0],
- schema_path=["items"],
- )
- def test_multiple_item_paths(self):
- self.assertShows(
- """
- Failed validating 'type' in schema['items'][0]:
- {'type': 'string'}
- On instance[0]['a']:
- 5
- """,
- path=[0, "a"],
- schema_path=["items", 0, 1],
- )
- def test_uses_pprint(self):
- self.assertShows(
- """
- Failed validating 'maxLength' in schema:
- {0: 0,
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- 7: 7,
- 8: 8,
- 9: 9,
- 10: 10,
- 11: 11,
- 12: 12,
- 13: 13,
- 14: 14,
- 15: 15,
- 16: 16,
- 17: 17,
- 18: 18,
- 19: 19}
- On instance:
- [0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 10,
- 11,
- 12,
- 13,
- 14,
- 15,
- 16,
- 17,
- 18,
- 19,
- 20,
- 21,
- 22,
- 23,
- 24]
- """,
- instance=list(range(25)),
- schema=dict(zip(range(20), range(20))),
- validator="maxLength",
- )
- def test_str_works_with_instances_having_overriden_eq_operator(self):
- """
- Check for #164 which rendered exceptions unusable when a
- `ValidationError` involved instances with an `__eq__` method
- that returned truthy values.
- """
- class DontEQMeBro:
- def __eq__(this, other): # pragma: no cover
- self.fail("Don't!")
- def __ne__(this, other): # pragma: no cover
- self.fail("Don't!")
- instance = DontEQMeBro()
- error = exceptions.ValidationError(
- "a message",
- validator="foo",
- instance=instance,
- validator_value="some",
- schema="schema",
- )
- self.assertIn(repr(instance), str(error))
- class TestHashable(TestCase):
- def test_hashable(self):
- {exceptions.ValidationError("")}
- {exceptions.SchemaError("")}
|