Skip to content

Commit a677cbe

Browse files
committed
Merge branch 'pr/1300'
* pr/1300: [pre-commit.ci] auto fixes from pre-commit.com hooks Pick the deepest error among the most relevant ones in each separate subschema # Conflicts: # jsonschema/exceptions.py
2 parents cfc180e + 88ec636 commit a677cbe

2 files changed

Lines changed: 207 additions & 7 deletions

File tree

‎jsonschema/exceptions.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,6 @@ def relevance(error):
437437
validator = error.validator
438438
return ( # prefer errors which are ...
439439
-len(error.path), # shorter path thereby more general
440-
error.path, # earlier (for sibling errors)
441440
validator not in weak, # for a non-low-priority keyword
442441
validator in strong, # for a high priority keyword
443442
not error._matches_type(), # at least match the instance's type
@@ -467,7 +466,8 @@ def best_match(errors, key=relevance):
467466
since they indicate "more" is wrong with the instance.
468467
469468
If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
470-
*opposite* assumption is made -- i.e. the deepest error is picked,
469+
*opposite* assumption is made -- i.e. the deepest error is picked
470+
among the most relevant errors in each separate subschema,
471471
since these keywords only need to match once, and any other errors
472472
may not be relevant.
473473
@@ -502,9 +502,19 @@ def best_match(errors, key=relevance):
502502
return
503503

504504
while best.context:
505+
# Calculate the most relevant error in each separate subschema
506+
best_in_subschemas = []
507+
for error in best.context:
508+
index = error.schema_path[0]
509+
if index == len(best_in_subschemas):
510+
best_in_subschemas.append(error)
511+
else:
512+
prev = best_in_subschemas[index]
513+
best_in_subschemas[index] = max(prev, error, key=key)
514+
505515
# Calculate the minimum via nsmallest, because we don't recurse if
506516
# all nested errors have the same relevance (i.e. if min == max == all)
507-
smallest = heapq.nsmallest(2, best.context, key=key)
517+
smallest = heapq.nsmallest(2, best_in_subschemas, key=key)
508518
if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004
509519
return best
510520
best = smallest[0]

‎jsonschema/tests/test_exceptions.py‎

Lines changed: 194 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,53 @@ def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self):
7272
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
7373
self.assertEqual(best.validator_value, "array")
7474

75+
def test_anyOf_traversal_for_single_shallower_errors_better_match(self):
76+
"""
77+
Traverse the context of an anyOf error with the only subschema,
78+
and select the most relevant error.
79+
"""
80+
81+
schema = {
82+
"anyOf": [
83+
{
84+
"properties": {
85+
"foo": {
86+
"minProperties": 2,
87+
"properties": {"bar": {"type": "object"}},
88+
},
89+
},
90+
},
91+
],
92+
}
93+
best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
94+
self.assertEqual(best.validator, "minProperties")
95+
96+
def test_anyOf_traversal_least_relevant_among_most_relevant_errors(self):
97+
"""
98+
Traverse the context of an anyOf error, and select
99+
the *least* relevant error among the most relevant errors
100+
in each separate subschema.
101+
102+
I.e. since only one of the schemas must match, we look for the most
103+
specific one, and choose the most relevant error produced by it.
104+
"""
105+
106+
schema = {
107+
"anyOf": [
108+
{"type": "string"},
109+
{
110+
"properties": {
111+
"foo": {
112+
"minProperties": 2,
113+
"properties": {"bar": {"type": "object"}},
114+
},
115+
},
116+
},
117+
],
118+
}
119+
best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
120+
self.assertEqual(best.validator, "minProperties")
121+
75122
def test_no_anyOf_traversal_for_equally_relevant_errors(self):
76123
"""
77124
We don't traverse into an anyOf (as above) if all of its context errors
@@ -88,6 +135,53 @@ def test_no_anyOf_traversal_for_equally_relevant_errors(self):
88135
best = self.best_match_of(instance=[], schema=schema)
89136
self.assertEqual(best.validator, "anyOf")
90137

138+
def test_no_anyOf_traversal_for_two_properties_sibling_errors(self):
139+
"""
140+
We don't traverse into an anyOf if all of its context errors
141+
seem to be equally "wrong" against the instance.
142+
"""
143+
144+
schema = {
145+
"anyOf": [
146+
{"properties": {"foo": {"type": "string"}}},
147+
{"properties": {"bar": {"type": "string"}}},
148+
],
149+
}
150+
best = self.best_match_of(instance={"foo": 1, "bar": 1}, schema=schema)
151+
self.assertEqual(best.validator, "anyOf")
152+
153+
def test_no_anyOf_traversal_for_two_items_sibling_errors(self):
154+
"""
155+
We don't traverse into an anyOf if all of its context errors
156+
seem to be equally "wrong" against the instance.
157+
"""
158+
159+
schema = {
160+
"anyOf": [
161+
{
162+
"type": "array",
163+
"items": {
164+
"$ref": "#/$defs/int_array",
165+
},
166+
},
167+
{
168+
"$ref": "#/$defs/int_array",
169+
},
170+
],
171+
"$defs": {
172+
"int_array": {
173+
"type": "array",
174+
"items": {
175+
"type": "integer",
176+
},
177+
},
178+
},
179+
}
180+
best = self.best_match_of(instance=["not an int", 0], schema=schema)
181+
self.assertEqual(best.validator, "anyOf")
182+
best = self.best_match_of(instance=[0, "not an int"], schema=schema)
183+
self.assertEqual(best.validator, "anyOf")
184+
91185
def test_anyOf_traversal_for_single_equally_relevant_error(self):
92186
"""
93187
We *do* traverse anyOf with a single nested error, even though it is
@@ -102,7 +196,7 @@ def test_anyOf_traversal_for_single_equally_relevant_error(self):
102196
best = self.best_match_of(instance=[], schema=schema)
103197
self.assertEqual(best.validator, "type")
104198

105-
def test_anyOf_traversal_for_single_sibling_errors(self):
199+
def test_anyOf_traversal_for_single_sibling_errors_choose_first(self):
106200
"""
107201
We *do* traverse anyOf with a single subschema that fails multiple
108202
times (e.g. on multiple items).
@@ -113,8 +207,9 @@ def test_anyOf_traversal_for_single_sibling_errors(self):
113207
{"items": {"const": 37}},
114208
],
115209
}
116-
best = self.best_match_of(instance=[12, 12], schema=schema)
210+
best = self.best_match_of(instance=[12, 13], schema=schema)
117211
self.assertEqual(best.validator, "const")
212+
self.assertEqual(best.instance, 12)
118213

119214
def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):
120215
"""
@@ -154,6 +249,53 @@ def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
154249
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
155250
self.assertEqual(best.validator_value, "array")
156251

252+
def test_oneOf_traversal_for_single_shallower_errors_better_match(self):
253+
"""
254+
Traverse the context of an oneOf error with the only subschema,
255+
and select the most relevant error.
256+
"""
257+
258+
schema = {
259+
"oneOf": [
260+
{
261+
"properties": {
262+
"foo": {
263+
"minProperties": 2,
264+
"properties": {"bar": {"type": "object"}},
265+
},
266+
},
267+
},
268+
],
269+
}
270+
best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
271+
self.assertEqual(best.validator, "minProperties")
272+
273+
def test_oneOf_traversal_least_relevant_among_most_relevant_errors(self):
274+
"""
275+
Traverse the context of an oneOf error, and select
276+
the *least* relevant error among the most relevant errors
277+
in each separate subschema.
278+
279+
I.e. since only one of the schemas must match, we look for the most
280+
specific one, and choose the most relevant error produced by it.
281+
"""
282+
283+
schema = {
284+
"oneOf": [
285+
{"type": "string"},
286+
{
287+
"properties": {
288+
"foo": {
289+
"minProperties": 2,
290+
"properties": {"bar": {"type": "object"}},
291+
},
292+
},
293+
},
294+
],
295+
}
296+
best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema)
297+
self.assertEqual(best.validator, "minProperties")
298+
157299
def test_no_oneOf_traversal_for_equally_relevant_errors(self):
158300
"""
159301
We don't traverse into an oneOf (as above) if all of its context errors
@@ -170,6 +312,53 @@ def test_no_oneOf_traversal_for_equally_relevant_errors(self):
170312
best = self.best_match_of(instance=[], schema=schema)
171313
self.assertEqual(best.validator, "oneOf")
172314

315+
def test_no_oneOf_traversal_for_two_properties_sibling_errors(self):
316+
"""
317+
We don't traverse into an oneOf if all of its context errors
318+
seem to be equally "wrong" against the instance.
319+
"""
320+
321+
schema = {
322+
"oneOf": [
323+
{"properties": {"foo": {"type": "string"}}},
324+
{"properties": {"bar": {"type": "string"}}},
325+
],
326+
}
327+
best = self.best_match_of(instance={"foo": 1, "bar": 1}, schema=schema)
328+
self.assertEqual(best.validator, "oneOf")
329+
330+
def test_no_oneOf_traversal_for_two_items_sibling_errors(self):
331+
"""
332+
We don't traverse into an anyOf if all of its context errors
333+
seem to be equally "wrong" against the instance.
334+
"""
335+
336+
schema = {
337+
"oneOf": [
338+
{
339+
"type": "array",
340+
"items": {
341+
"$ref": "#/$defs/int_array",
342+
},
343+
},
344+
{
345+
"$ref": "#/$defs/int_array",
346+
},
347+
],
348+
"$defs": {
349+
"int_array": {
350+
"type": "array",
351+
"items": {
352+
"type": "integer",
353+
},
354+
},
355+
},
356+
}
357+
best = self.best_match_of(instance=["not an int", 0], schema=schema)
358+
self.assertEqual(best.validator, "oneOf")
359+
best = self.best_match_of(instance=[0, "not an int"], schema=schema)
360+
self.assertEqual(best.validator, "oneOf")
361+
173362
def test_oneOf_traversal_for_single_equally_relevant_error(self):
174363
"""
175364
We *do* traverse oneOf with a single nested error, even though it is
@@ -184,7 +373,7 @@ def test_oneOf_traversal_for_single_equally_relevant_error(self):
184373
best = self.best_match_of(instance=[], schema=schema)
185374
self.assertEqual(best.validator, "type")
186375

187-
def test_oneOf_traversal_for_single_sibling_errors(self):
376+
def test_oneOf_traversal_for_single_sibling_errors_choose_first(self):
188377
"""
189378
We *do* traverse oneOf with a single subschema that fails multiple
190379
times (e.g. on multiple items).
@@ -195,8 +384,9 @@ def test_oneOf_traversal_for_single_sibling_errors(self):
195384
{"items": {"const": 37}},
196385
],
197386
}
198-
best = self.best_match_of(instance=[12, 12], schema=schema)
387+
best = self.best_match_of(instance=[12, 13], schema=schema)
199388
self.assertEqual(best.validator, "const")
389+
self.assertEqual(best.instance, 12)
200390

201391
def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):
202392
"""

0 commit comments

Comments
 (0)