Skip to content

Commit 996437f

Browse files
committed
Add Validator.evolve, deprecating passing _schema to methods.
A Validator should be thought of as encapsulating validation with a single fixed schema. Previously, iter_errors and is_valid allowed passing a second argument, which was a different schema to use for one method call. This was mostly for convenience, since the second argument is often used during sub-validation whilst say, recursing. The correct way to do so now is to say: validator.evolve(schema=new_schema).iter_errors(...) validator.evolve(schema=new_schema).is_valid(...) instead, which is essentially equally convenient. Closes: #522
1 parent 4521697 commit 996437f

File tree

6 files changed

+99
-20
lines changed

6 files changed

+99
-20
lines changed

docs/validate.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ classes should adhere to.
151151
...
152152
ValidationError: [2, 3, 4] is too long
153153

154+
.. method:: evolve(**kwargs)
155+
156+
Create a new validator like this one, but with given changes.
157+
158+
Preserves all other attributes, so can be used to e.g. create a
159+
validator with a different schema but with the same :validator:`$ref`
160+
resolution behavior.
161+
162+
>>> validator = Draft202012Validator({})
163+
>>> validator.evolve(schema={"type": "number"})
164+
Draft202012Validator(schema={'type': 'number'}, format_checker=None)
165+
154166

155167
All of the `versioned validators <versioned-validators>` that are included with
156168
`jsonschema` adhere to the interface, and implementers of validator classes

jsonschema/_legacy_validators.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def dependencies_draft4_draft6_draft7(
7272

7373
def disallow_draft3(validator, disallow, instance, schema):
7474
for disallowed in _utils.ensure_list(disallow):
75-
if validator.is_valid(instance, {"type": [disallowed]}):
75+
if validator.evolve(schema={"type": [disallowed]}).is_valid(instance):
7676
message = f"{disallowed!r} is disallowed for {instance!r}"
7777
yield ValidationError(message)
7878

@@ -200,7 +200,10 @@ def contains_draft6_draft7(validator, contains, instance, schema):
200200
if not validator.is_type(instance, "array"):
201201
return
202202

203-
if not any(validator.is_valid(element, contains) for element in instance):
203+
if not any(
204+
validator.evolve(schema=contains).is_valid(element)
205+
for element in instance
206+
):
204207
yield ValidationError(
205208
f"None of {instance!r} are valid under the given schema",
206209
)

jsonschema/_utils.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
240240
evaluated_indexes += list(range(0, len(schema["prefixItems"])))
241241

242242
if "if" in schema:
243-
if validator.is_valid(instance, schema["if"]):
243+
if validator.evolve(schema=schema["if"]).is_valid(instance):
244244
evaluated_indexes += find_evaluated_item_indexes_by_schema(
245245
validator, instance, schema["if"],
246246
)
@@ -257,7 +257,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
257257
for keyword in ["contains", "unevaluatedItems"]:
258258
if keyword in schema:
259259
for k, v in enumerate(instance):
260-
if validator.is_valid(v, schema[keyword]):
260+
if validator.evolve(schema=schema[keyword]).is_valid(v):
261261
evaluated_indexes.append(k)
262262

263263
for keyword in ["allOf", "oneOf", "anyOf"]:
@@ -301,22 +301,24 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
301301
if keyword in schema:
302302
if validator.is_type(schema[keyword], "boolean"):
303303
for property, value in instance.items():
304-
if validator.is_valid({property: value}, schema[keyword]):
304+
if validator.evolve(schema=schema[keyword]).is_valid(
305+
{property: value},
306+
):
305307
evaluated_keys.append(property)
306308

307309
if validator.is_type(schema[keyword], "object"):
308310
for property, subschema in schema[keyword].items():
309-
if property in instance and validator.is_valid(
310-
instance[property], subschema,
311-
):
311+
if property in instance and validator.evolve(
312+
schema=subschema,
313+
).is_valid(instance[property]):
312314
evaluated_keys.append(property)
313315

314316
if "patternProperties" in schema:
315317
for property, value in instance.items():
316318
for pattern, _ in schema["patternProperties"].items():
317-
if re.search(pattern, property) and validator.is_valid(
318-
{property: value}, schema["patternProperties"],
319-
):
319+
if re.search(pattern, property) and validator.evolve(
320+
schema=schema["patternProperties"],
321+
).is_valid({property: value}):
320322
evaluated_keys.append(property)
321323

322324
if "dependentSchemas" in schema:
@@ -337,7 +339,7 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
337339
)
338340

339341
if "if" in schema:
340-
if validator.is_valid(instance, schema["if"]):
342+
if validator.evolve(schema=schema["if"]).is_valid(instance):
341343
evaluated_keys += find_evaluated_property_keys_by_schema(
342344
validator, instance, schema["if"],
343345
)

jsonschema/_validators.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def contains(validator, contains, instance, schema):
112112
max_contains = schema.get("maxContains", len(instance))
113113

114114
for each in instance:
115-
if validator.is_valid(each, contains):
115+
if validator.evolve(schema=contains).is_valid(each):
116116
matches += 1
117117
if matches > max_contains:
118118
yield ValidationError(
@@ -388,21 +388,24 @@ def oneOf(validator, oneOf, instance, schema):
388388
context=all_errors,
389389
)
390390

391-
more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)]
391+
more_valid = [
392+
each for _, each in subschemas
393+
if validator.evolve(schema=each).is_valid(instance)
394+
]
392395
if more_valid:
393396
more_valid.append(first_valid)
394397
reprs = ", ".join(repr(schema) for schema in more_valid)
395398
yield ValidationError(f"{instance!r} is valid under each of {reprs}")
396399

397400

398401
def not_(validator, not_schema, instance, schema):
399-
if validator.is_valid(instance, not_schema):
402+
if validator.evolve(schema=not_schema).is_valid(instance):
400403
message = f"{instance!r} should not be valid under {not_schema!r}"
401404
yield ValidationError(message)
402405

403406

404407
def if_(validator, if_schema, instance, schema):
405-
if validator.is_valid(instance, if_schema):
408+
if validator.evolve(schema=if_schema).is_valid(instance):
406409
if "then" in schema:
407410
then = schema["then"]
408411
yield from validator.descend(instance, then, schema_path="then")

jsonschema/tests/test_deprecations.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest import TestCase
22

3-
from jsonschema.validators import RefResolver
3+
from jsonschema.validators import Draft7Validator, RefResolver
44

55

66
class TestDeprecations(TestCase):
@@ -48,3 +48,37 @@ def test_RefResolver_in_scope(self):
4848
"jsonschema.RefResolver.in_scope is deprecated ",
4949
),
5050
)
51+
52+
def test_Validator_is_valid_two_arguments(self):
53+
"""
54+
As of v4.0.0, calling is_valid with two arguments (to provide a
55+
different schema) is deprecated.
56+
"""
57+
58+
validator = Draft7Validator({})
59+
with self.assertWarns(DeprecationWarning) as w:
60+
result = validator.is_valid("foo", {"type": "number"})
61+
62+
self.assertFalse(result)
63+
self.assertTrue(
64+
str(w.warning).startswith(
65+
"Passing a schema to Validator.is_valid is deprecated ",
66+
),
67+
)
68+
69+
def test_Validator_iter_errors_two_arguments(self):
70+
"""
71+
As of v4.0.0, calling iter_errors with two arguments (to provide a
72+
different schema) is deprecated.
73+
"""
74+
75+
validator = Draft7Validator({})
76+
with self.assertWarns(DeprecationWarning) as w:
77+
error, = validator.iter_errors("foo", {"type": "number"})
78+
79+
self.assertEqual(error.validator, "type")
80+
self.assertTrue(
81+
str(w.warning).startswith(
82+
"Passing a schema to Validator.iter_errors is deprecated ",
83+
),
84+
)

jsonschema/validators.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,21 @@ def check_schema(cls, schema):
172172
for error in cls(cls.META_SCHEMA).iter_errors(schema):
173173
raise exceptions.SchemaError.create_from(error)
174174

175+
def evolve(self, **kwargs):
176+
return attr.evolve(self, **kwargs)
177+
175178
def iter_errors(self, instance, _schema=None):
176-
if _schema is None:
179+
if _schema is not None:
180+
warnings.warn(
181+
(
182+
"Passing a schema to Validator.iter_errors "
183+
"is deprecated and will be removed in a future "
184+
"release. Call validator.evolve(schema=new_schema)."
185+
"iter_errors(...) instead."
186+
),
187+
DeprecationWarning,
188+
)
189+
else:
177190
_schema = self.schema
178191

179192
if _schema is True:
@@ -214,7 +227,7 @@ def iter_errors(self, instance, _schema=None):
214227
self.resolver.pop_scope()
215228

216229
def descend(self, instance, schema, path=None, schema_path=None):
217-
for error in self.iter_errors(instance, schema):
230+
for error in self.evolve(schema=schema).iter_errors(instance):
218231
if path is not None:
219232
error.path.appendleft(path)
220233
if schema_path is not None:
@@ -232,7 +245,19 @@ def is_type(self, instance, type):
232245
raise exceptions.UnknownType(type, instance, self.schema)
233246

234247
def is_valid(self, instance, _schema=None):
235-
error = next(self.iter_errors(instance, _schema), None)
248+
if _schema is not None:
249+
warnings.warn(
250+
(
251+
"Passing a schema to Validator.is_valid is deprecated "
252+
"and will be removed in a future release. Call "
253+
"validator.evolve(schema=new_schema).is_valid(...) "
254+
"instead."
255+
),
256+
DeprecationWarning,
257+
)
258+
self = self.evolve(schema=_schema)
259+
260+
error = next(self.iter_errors(instance), None)
236261
return error is None
237262

238263
if version is not None:

0 commit comments

Comments
 (0)