codekingpro/portable-devtools
114k
1from unittest import TestCase2import textwrap3 4import jsonpath_ng5 6from jsonschema import exceptions7from jsonschema.validators import _LATEST_VERSION8 9 10class TestBestMatch(TestCase):11 def best_match_of(self, instance, schema):12 errors = list(_LATEST_VERSION(schema).iter_errors(instance))13 msg = f"No errors found for {instance} under {schema!r}!"14 self.assertTrue(errors, msg=msg)15 16 best = exceptions.best_match(iter(errors))17 reversed_best = exceptions.best_match(reversed(errors))18 19 self.assertEqual(20 best._contents(),21 reversed_best._contents(),22 f"No consistent best match!\nGot: {best}\n\nThen: {reversed_best}",23 )24 return best25 26 def test_shallower_errors_are_better_matches(self):27 schema = {28 "properties": {29 "foo": {30 "minProperties": 2,31 "properties": {"bar": {"type": "object"}},32 },33 },34 }35 best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)36 self.assertEqual(best.validator, "minProperties")37 38 def test_oneOf_and_anyOf_are_weak_matches(self):39 """40 A property you *must* match is probably better than one you have to41 match a part of.42 """43 44 schema = {45 "minProperties": 2,46 "anyOf": [{"type": "string"}, {"type": "number"}],47 "oneOf": [{"type": "string"}, {"type": "number"}],48 }49 best = self.best_match_of(instance={}, schema=schema)50 self.assertEqual(best.validator, "minProperties")51 52 def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self):53 """54 If the most relevant error is an anyOf, then we traverse its context55 and select the otherwise *least* relevant error, since in this case56 that means the most specific, deep, error inside the instance.57 58 I.e. since only one of the schemas must match, we look for the most59 relevant one.60 """61 62 schema = {63 "properties": {64 "foo": {65 "anyOf": [66 {"type": "string"},67 {"properties": {"bar": {"type": "array"}}},68 ],69 },70 },71 }72 best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)73 self.assertEqual(best.validator_value, "array")74 75 def test_no_anyOf_traversal_for_equally_relevant_errors(self):76 """77 We don't traverse into an anyOf (as above) if all of its context errors78 seem to be equally "wrong" against the instance.79 """80 81 schema = {82 "anyOf": [83 {"type": "string"},84 {"type": "integer"},85 {"type": "object"},86 ],87 }88 best = self.best_match_of(instance=[], schema=schema)89 self.assertEqual(best.validator, "anyOf")90 91 def test_anyOf_traversal_for_single_equally_relevant_error(self):92 """93 We *do* traverse anyOf with a single nested error, even though it is94 vacuously equally relevant to itself.95 """96 97 schema = {98 "anyOf": [99 {"type": "string"},100 ],101 }102 best = self.best_match_of(instance=[], schema=schema)103 self.assertEqual(best.validator, "type")104 105 def test_anyOf_traversal_for_single_sibling_errors(self):106 """107 We *do* traverse anyOf with a single subschema that fails multiple108 times (e.g. on multiple items).109 """110 111 schema = {112 "anyOf": [113 {"items": {"const": 37}},114 ],115 }116 best = self.best_match_of(instance=[12, 12], schema=schema)117 self.assertEqual(best.validator, "const")118 119 def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):120 """121 We *do* traverse anyOf with multiple subschemas when one does not type122 match.123 """124 125 schema = {126 "anyOf": [127 {"type": "object"},128 {"items": {"const": 37}},129 ],130 }131 best = self.best_match_of(instance=[12, 12], schema=schema)132 self.assertEqual(best.validator, "const")133 134 def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):135 """136 If the most relevant error is an oneOf, then we traverse its context137 and select the otherwise *least* relevant error, since in this case138 that means the most specific, deep, error inside the instance.139 140 I.e. since only one of the schemas must match, we look for the most141 relevant one.142 """143 144 schema = {145 "properties": {146 "foo": {147 "oneOf": [148 {"type": "string"},149 {"properties": {"bar": {"type": "array"}}},150 ],151 },152 },153 }154 best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)155 self.assertEqual(best.validator_value, "array")156 157 def test_no_oneOf_traversal_for_equally_relevant_errors(self):158 """159 We don't traverse into an oneOf (as above) if all of its context errors160 seem to be equally "wrong" against the instance.161 """162 163 schema = {164 "oneOf": [165 {"type": "string"},166 {"type": "integer"},167 {"type": "object"},168 ],169 }170 best = self.best_match_of(instance=[], schema=schema)171 self.assertEqual(best.validator, "oneOf")172 173 def test_oneOf_traversal_for_single_equally_relevant_error(self):174 """175 We *do* traverse oneOf with a single nested error, even though it is176 vacuously equally relevant to itself.177 """178 179 schema = {180 "oneOf": [181 {"type": "string"},182 ],183 }184 best = self.best_match_of(instance=[], schema=schema)185 self.assertEqual(best.validator, "type")186 187 def test_oneOf_traversal_for_single_sibling_errors(self):188 """189 We *do* traverse oneOf with a single subschema that fails multiple190 times (e.g. on multiple items).191 """192 193 schema = {194 "oneOf": [195 {"items": {"const": 37}},196 ],197 }198 best = self.best_match_of(instance=[12, 12], schema=schema)199 self.assertEqual(best.validator, "const")200 201 def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):202 """203 We *do* traverse oneOf with multiple subschemas when one does not type204 match.205 """206 207 schema = {208 "oneOf": [209 {"type": "object"},210 {"items": {"const": 37}},211 ],212 }213 best = self.best_match_of(instance=[12, 12], schema=schema)214 self.assertEqual(best.validator, "const")215 216 def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):217 """218 Now, if the error is allOf, we traverse but select the *most* relevant219 error from the context, because all schemas here must match anyways.220 """221 222 schema = {223 "properties": {224 "foo": {225 "allOf": [226 {"type": "string"},227 {"properties": {"bar": {"type": "array"}}},228 ],229 },230 },231 }232 best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)233 self.assertEqual(best.validator_value, "string")234 235 def test_nested_context_for_oneOf(self):236 """237 We traverse into nested contexts (a oneOf containing an error in a238 nested oneOf here).239 """240 241 schema = {242 "properties": {243 "foo": {244 "oneOf": [245 {"type": "string"},246 {247 "oneOf": [248 {"type": "string"},249 {250 "properties": {251 "bar": {"type": "array"},252 },253 },254 ],255 },256 ],257 },258 },259 }260 best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)261 self.assertEqual(best.validator_value, "array")262 263 def test_it_prioritizes_matching_types(self):264 schema = {265 "properties": {266 "foo": {267 "anyOf": [268 {"type": "array", "minItems": 2},269 {"type": "string", "minLength": 10},270 ],271 },272 },273 }274 best = self.best_match_of(instance={"foo": "bar"}, schema=schema)275 self.assertEqual(best.validator, "minLength")276 277 reordered = {278 "properties": {279 "foo": {280 "anyOf": [281 {"type": "string", "minLength": 10},282 {"type": "array", "minItems": 2},283 ],284 },285 },286 }287 best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)288 self.assertEqual(best.validator, "minLength")289 290 def test_it_prioritizes_matching_union_types(self):291 schema = {292 "properties": {293 "foo": {294 "anyOf": [295 {"type": ["array", "object"], "minItems": 2},296 {"type": ["integer", "string"], "minLength": 10},297 ],298 },299 },300 }301 best = self.best_match_of(instance={"foo": "bar"}, schema=schema)302 self.assertEqual(best.validator, "minLength")303 304 reordered = {305 "properties": {306 "foo": {307 "anyOf": [308 {"type": "string", "minLength": 10},309 {"type": "array", "minItems": 2},310 ],311 },312 },313 }314 best = self.best_match_of(instance={"foo": "bar"}, schema=reordered)315 self.assertEqual(best.validator, "minLength")316 317 def test_boolean_schemas(self):318 schema = {"properties": {"foo": False}}319 best = self.best_match_of(instance={"foo": "bar"}, schema=schema)320 self.assertIsNone(best.validator)321 322 def test_one_error(self):323 validator = _LATEST_VERSION({"minProperties": 2})324 validator.iter_errors({})325 self.assertEqual(326 exceptions.best_match(validator.iter_errors({})).validator,327 "minProperties",328 )329 330 def test_no_errors(self):331 validator = _LATEST_VERSION({})332 self.assertIsNone(exceptions.best_match(validator.iter_errors({})))333 334 335class TestByRelevance(TestCase):336 def test_short_paths_are_better_matches(self):337 shallow = exceptions.ValidationError("Oh no!", path=["baz"])338 deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"])339 match = max([shallow, deep], key=exceptions.relevance)340 self.assertIs(match, shallow)341 342 match = max([deep, shallow], key=exceptions.relevance)343 self.assertIs(match, shallow)344 345 def test_global_errors_are_even_better_matches(self):346 shallow = exceptions.ValidationError("Oh no!", path=[])347 deep = exceptions.ValidationError("Oh yes!", path=["foo"])348 349 errors = sorted([shallow, deep], key=exceptions.relevance)350 self.assertEqual(351 [list(error.path) for error in errors],352 [["foo"], []],353 )354 355 errors = sorted([deep, shallow], key=exceptions.relevance)356 self.assertEqual(357 [list(error.path) for error in errors],358 [["foo"], []],359 )360 361 def test_weak_keywords_are_lower_priority(self):362 weak = exceptions.ValidationError("Oh no!", path=[], validator="a")363 normal = exceptions.ValidationError("Oh yes!", path=[], validator="b")364 365 best_match = exceptions.by_relevance(weak="a")366 367 match = max([weak, normal], key=best_match)368 self.assertIs(match, normal)369 370 match = max([normal, weak], key=best_match)371 self.assertIs(match, normal)372 373 def test_strong_keywords_are_higher_priority(self):374 weak = exceptions.ValidationError("Oh no!", path=[], validator="a")375 normal = exceptions.ValidationError("Oh yes!", path=[], validator="b")376 strong = exceptions.ValidationError("Oh fine!", path=[], validator="c")377 378 best_match = exceptions.by_relevance(weak="a", strong="c")379 380 match = max([weak, normal, strong], key=best_match)381 self.assertIs(match, strong)382 383 match = max([strong, normal, weak], key=best_match)384 self.assertIs(match, strong)385 386 387class TestErrorTree(TestCase):388 def test_it_knows_how_many_total_errors_it_contains(self):389 # FIXME: #442390 errors = [391 exceptions.ValidationError("Something", validator=i)392 for i in range(8)393 ]394 tree = exceptions.ErrorTree(errors)395 self.assertEqual(tree.total_errors, 8)396 397 def test_it_contains_an_item_if_the_item_had_an_error(self):398 errors = [exceptions.ValidationError("a message", path=["bar"])]399 tree = exceptions.ErrorTree(errors)400 self.assertIn("bar", tree)401 402 def test_it_does_not_contain_an_item_if_the_item_had_no_error(self):403 errors = [exceptions.ValidationError("a message", path=["bar"])]404 tree = exceptions.ErrorTree(errors)405 self.assertNotIn("foo", tree)406 407 def test_keywords_that_failed_appear_in_errors_dict(self):408 error = exceptions.ValidationError("a message", validator="foo")409 tree = exceptions.ErrorTree([error])410 self.assertEqual(tree.errors, {"foo": error})411 412 def test_it_creates_a_child_tree_for_each_nested_path(self):413 errors = [414 exceptions.ValidationError("a bar message", path=["bar"]),415 exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]),416 ]417 tree = exceptions.ErrorTree(errors)418 self.assertIn(0, tree["bar"])419 self.assertNotIn(1, tree["bar"])420 421 def test_children_have_their_errors_dicts_built(self):422 e1, e2 = (423 exceptions.ValidationError("1", validator="foo", path=["bar", 0]),424 exceptions.ValidationError("2", validator="quux", path=["bar", 0]),425 )426 tree = exceptions.ErrorTree([e1, e2])427 self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2})428 429 def test_multiple_errors_with_instance(self):430 e1, e2 = (431 exceptions.ValidationError(432 "1",433 validator="foo",434 path=["bar", "bar2"],435 instance="i1"),436 exceptions.ValidationError(437 "2",438 validator="quux",439 path=["foobar", 2],440 instance="i2"),441 )442 exceptions.ErrorTree([e1, e2])443 444 def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self):445 error = exceptions.ValidationError("123", validator="foo", instance=[])446 tree = exceptions.ErrorTree([error])447 448 with self.assertRaises(IndexError):449 tree[0]450 451 def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self):452 """453 If a keyword refers to a path that isn't in the instance, the454 tree still properly returns a subtree for that path.455 """456 457 error = exceptions.ValidationError(458 "a message", validator="foo", instance={}, path=["foo"],459 )460 tree = exceptions.ErrorTree([error])461 self.assertIsInstance(tree["foo"], exceptions.ErrorTree)462 463 def test_iter(self):464 e1, e2 = (465 exceptions.ValidationError(466 "1",467 validator="foo",468 path=["bar", "bar2"],469 instance="i1"),470 exceptions.ValidationError(471 "2",472 validator="quux",473 path=["foobar", 2],474 instance="i2"),475 )476 tree = exceptions.ErrorTree([e1, e2])477 self.assertEqual(set(tree), {"bar", "foobar"})478 479 def test_repr_single(self):480 error = exceptions.ValidationError(481 "1",482 validator="foo",483 path=["bar", "bar2"],484 instance="i1",485 )486 tree = exceptions.ErrorTree([error])487 self.assertEqual(repr(tree), "<ErrorTree (1 total error)>")488 489 def test_repr_multiple(self):490 e1, e2 = (491 exceptions.ValidationError(492 "1",493 validator="foo",494 path=["bar", "bar2"],495 instance="i1"),496 exceptions.ValidationError(497 "2",498 validator="quux",499 path=["foobar", 2],500 instance="i2"),501 )502 tree = exceptions.ErrorTree([e1, e2])503 self.assertEqual(repr(tree), "<ErrorTree (2 total errors)>")504 505 def test_repr_empty(self):506 tree = exceptions.ErrorTree([])507 self.assertEqual(repr(tree), "<ErrorTree (0 total errors)>")508 509 510class TestErrorInitReprStr(TestCase):511 def make_error(self, **kwargs):512 defaults = dict(513 message="hello",514 validator="type",515 validator_value="string",516 instance=5,517 schema={"type": "string"},518 )519 defaults.update(kwargs)520 return exceptions.ValidationError(**defaults)521 522 def assertShows(self, expected, **kwargs):523 expected = textwrap.dedent(expected).rstrip("\n")524 525 error = self.make_error(**kwargs)526 message_line, _, rest = str(error).partition("\n")527 self.assertEqual(message_line, error.message)528 self.assertEqual(rest, expected)529 530 def test_it_calls_super_and_sets_args(self):531 error = self.make_error()532 self.assertGreater(len(error.args), 1)533 534 def test_repr(self):535 self.assertEqual(536 repr(exceptions.ValidationError(message="Hello!")),537 "<ValidationError: 'Hello!'>",538 )539 540 def test_unset_error(self):541 error = exceptions.ValidationError("message")542 self.assertEqual(str(error), "message")543 544 kwargs = {545 "validator": "type",546 "validator_value": "string",547 "instance": 5,548 "schema": {"type": "string"},549 }550 # Just the message should show if any of the attributes are unset551 for attr in kwargs:552 k = dict(kwargs)553 del k[attr]554 error = exceptions.ValidationError("message", **k)555 self.assertEqual(str(error), "message")556 557 def test_empty_paths(self):558 self.assertShows(559 """560 Failed validating 'type' in schema:561 {'type': 'string'}562 563 On instance:564 5565 """,566 path=[],567 schema_path=[],568 )569 570 def test_one_item_paths(self):571 self.assertShows(572 """573 Failed validating 'type' in schema:574 {'type': 'string'}575 576 On instance[0]:577 5578 """,579 path=[0],580 schema_path=["items"],581 )582 583 def test_multiple_item_paths(self):584 self.assertShows(585 """586 Failed validating 'type' in schema['items'][0]:587 {'type': 'string'}588 589 On instance[0]['a']:590 5591 """,592 path=[0, "a"],593 schema_path=["items", 0, 1],594 )595 596 def test_uses_pprint(self):597 self.assertShows(598 """599 Failed validating 'maxLength' in schema:600 {0: 0,601 1: 1,602 2: 2,603 3: 3,604 4: 4,605 5: 5,606 6: 6,607 7: 7,608 8: 8,609 9: 9,610 10: 10,611 11: 11,612 12: 12,613 13: 13,614 14: 14,615 15: 15,616 16: 16,617 17: 17,618 18: 18,619 19: 19}620 621 On instance:622 [0,623 1,624 2,625 3,626 4,627 5,628 6,629 7,630 8,631 9,632 10,633 11,634 12,635 13,636 14,637 15,638 16,639 17,640 18,641 19,642 20,643 21,644 22,645 23,646 24]647 """,648 instance=list(range(25)),649 schema=dict(zip(range(20), range(20))),650 validator="maxLength",651 )652 653 def test_does_not_reorder_dicts(self):654 self.assertShows(655 """656 Failed validating 'type' in schema:657 {'do': 3, 'not': 7, 'sort': 37, 'me': 73}658 659 On instance:660 {'here': 73, 'too': 37, 'no': 7, 'sorting': 3}661 """,662 schema={663 "do": 3,664 "not": 7,665 "sort": 37,666 "me": 73,667 },668 instance={669 "here": 73,670 "too": 37,671 "no": 7,672 "sorting": 3,673 },674 )675 676 def test_str_works_with_instances_having_overriden_eq_operator(self):677 """678 Check for #164 which rendered exceptions unusable when a679 `ValidationError` involved instances with an `__eq__` method680 that returned truthy values.681 """682 683 class DontEQMeBro:684 def __eq__(this, other): # pragma: no cover685 self.fail("Don't!")686 687 def __ne__(this, other): # pragma: no cover688 self.fail("Don't!")689 690 instance = DontEQMeBro()691 error = exceptions.ValidationError(692 "a message",693 validator="foo",694 instance=instance,695 validator_value="some",696 schema="schema",697 )698 self.assertIn(repr(instance), str(error))699 700 701class TestHashable(TestCase):702 def test_hashable(self):703 {exceptions.ValidationError("")}704 {exceptions.SchemaError("")}705 706 707class TestJsonPathRendering(TestCase):708 def validate_json_path_rendering(self, property_name, expected_path):709 error = exceptions.ValidationError(710 path=[property_name],711 message="1",712 validator="foo",713 instance="i1",714 )715 716 rendered_json_path = error.json_path717 self.assertEqual(rendered_json_path, expected_path)718 719 re_parsed_name = jsonpath_ng.parse(rendered_json_path).right.fields[0]720 self.assertEqual(re_parsed_name, property_name)721 722 def test_basic(self):723 self.validate_json_path_rendering("x", "$.x")724 725 def test_empty(self):726 self.validate_json_path_rendering("", "$['']")727 728 def test_number(self):729 self.validate_json_path_rendering("1", "$['1']")730 731 def test_period(self):732 self.validate_json_path_rendering(".", "$['.']")733 734 def test_single_quote(self):735 self.validate_json_path_rendering("'", r"$['\'']")736 737 def test_space(self):738 self.validate_json_path_rendering(" ", "$[' ']")739 740 def test_backslash(self):741 self.validate_json_path_rendering("\\", r"$['\\']")742 743 def test_backslash_single_quote(self):744 self.validate_json_path_rendering(r"\'", r"$['\\\'']")745 746 def test_underscore(self):747 self.validate_json_path_rendering("_", r"$['_']")748 749 def test_double_quote(self):750 self.validate_json_path_rendering('"', """$['"']""")751 752 def test_hyphen(self):753 self.validate_json_path_rendering("-", "$['-']")754 755 def test_json_path_injection(self):756 self.validate_json_path_rendering("a[0]", "$['a[0]']")757 758 def test_open_bracket(self):759 self.validate_json_path_rendering("[", "$['[']")760 