Skip to content

Commit

Permalink
Add Validator.evolve, deprecating passing _schema to methods.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Julian committed Aug 25, 2021
1 parent 4521697 commit 996437f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 20 deletions.
12 changes: 12 additions & 0 deletions docs/validate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ classes should adhere to.
...
ValidationError: [2, 3, 4] is too long

.. method:: evolve(**kwargs)

Create a new validator like this one, but with given changes.

Preserves all other attributes, so can be used to e.g. create a
validator with a different schema but with the same :validator:`$ref`
resolution behavior.

>>> validator = Draft202012Validator({})
>>> validator.evolve(schema={"type": "number"})
Draft202012Validator(schema={'type': 'number'}, format_checker=None)


All of the `versioned validators <versioned-validators>` that are included with
`jsonschema` adhere to the interface, and implementers of validator classes
Expand Down
7 changes: 5 additions & 2 deletions jsonschema/_legacy_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def dependencies_draft4_draft6_draft7(

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

Expand Down Expand Up @@ -200,7 +200,10 @@ def contains_draft6_draft7(validator, contains, instance, schema):
if not validator.is_type(instance, "array"):
return

if not any(validator.is_valid(element, contains) for element in instance):
if not any(
validator.evolve(schema=contains).is_valid(element)
for element in instance
):
yield ValidationError(
f"None of {instance!r} are valid under the given schema",
)
Expand Down
22 changes: 12 additions & 10 deletions jsonschema/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
evaluated_indexes += list(range(0, len(schema["prefixItems"])))

if "if" in schema:
if validator.is_valid(instance, schema["if"]):
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_indexes += find_evaluated_item_indexes_by_schema(
validator, instance, schema["if"],
)
Expand All @@ -257,7 +257,7 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
for keyword in ["contains", "unevaluatedItems"]:
if keyword in schema:
for k, v in enumerate(instance):
if validator.is_valid(v, schema[keyword]):
if validator.evolve(schema=schema[keyword]).is_valid(v):
evaluated_indexes.append(k)

for keyword in ["allOf", "oneOf", "anyOf"]:
Expand Down Expand Up @@ -301,22 +301,24 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
if keyword in schema:
if validator.is_type(schema[keyword], "boolean"):
for property, value in instance.items():
if validator.is_valid({property: value}, schema[keyword]):
if validator.evolve(schema=schema[keyword]).is_valid(
{property: value},
):
evaluated_keys.append(property)

if validator.is_type(schema[keyword], "object"):
for property, subschema in schema[keyword].items():
if property in instance and validator.is_valid(
instance[property], subschema,
):
if property in instance and validator.evolve(
schema=subschema,
).is_valid(instance[property]):
evaluated_keys.append(property)

if "patternProperties" in schema:
for property, value in instance.items():
for pattern, _ in schema["patternProperties"].items():
if re.search(pattern, property) and validator.is_valid(
{property: value}, schema["patternProperties"],
):
if re.search(pattern, property) and validator.evolve(
schema=schema["patternProperties"],
).is_valid({property: value}):
evaluated_keys.append(property)

if "dependentSchemas" in schema:
Expand All @@ -337,7 +339,7 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
)

if "if" in schema:
if validator.is_valid(instance, schema["if"]):
if validator.evolve(schema=schema["if"]).is_valid(instance):
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, schema["if"],
)
Expand Down
11 changes: 7 additions & 4 deletions jsonschema/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def contains(validator, contains, instance, schema):
max_contains = schema.get("maxContains", len(instance))

for each in instance:
if validator.is_valid(each, contains):
if validator.evolve(schema=contains).is_valid(each):
matches += 1
if matches > max_contains:
yield ValidationError(
Expand Down Expand Up @@ -388,21 +388,24 @@ def oneOf(validator, oneOf, instance, schema):
context=all_errors,
)

more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)]
more_valid = [
each for _, each in subschemas
if validator.evolve(schema=each).is_valid(instance)
]
if more_valid:
more_valid.append(first_valid)
reprs = ", ".join(repr(schema) for schema in more_valid)
yield ValidationError(f"{instance!r} is valid under each of {reprs}")


def not_(validator, not_schema, instance, schema):
if validator.is_valid(instance, not_schema):
if validator.evolve(schema=not_schema).is_valid(instance):
message = f"{instance!r} should not be valid under {not_schema!r}"
yield ValidationError(message)


def if_(validator, if_schema, instance, schema):
if validator.is_valid(instance, if_schema):
if validator.evolve(schema=if_schema).is_valid(instance):
if "then" in schema:
then = schema["then"]
yield from validator.descend(instance, then, schema_path="then")
Expand Down
36 changes: 35 additions & 1 deletion jsonschema/tests/test_deprecations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest import TestCase

from jsonschema.validators import RefResolver
from jsonschema.validators import Draft7Validator, RefResolver


class TestDeprecations(TestCase):
Expand Down Expand Up @@ -48,3 +48,37 @@ def test_RefResolver_in_scope(self):
"jsonschema.RefResolver.in_scope is deprecated ",
),
)

def test_Validator_is_valid_two_arguments(self):
"""
As of v4.0.0, calling is_valid with two arguments (to provide a
different schema) is deprecated.
"""

validator = Draft7Validator({})
with self.assertWarns(DeprecationWarning) as w:
result = validator.is_valid("foo", {"type": "number"})

self.assertFalse(result)
self.assertTrue(
str(w.warning).startswith(
"Passing a schema to Validator.is_valid is deprecated ",
),
)

def test_Validator_iter_errors_two_arguments(self):
"""
As of v4.0.0, calling iter_errors with two arguments (to provide a
different schema) is deprecated.
"""

validator = Draft7Validator({})
with self.assertWarns(DeprecationWarning) as w:
error, = validator.iter_errors("foo", {"type": "number"})

self.assertEqual(error.validator, "type")
self.assertTrue(
str(w.warning).startswith(
"Passing a schema to Validator.iter_errors is deprecated ",
),
)
31 changes: 28 additions & 3 deletions jsonschema/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,21 @@ def check_schema(cls, schema):
for error in cls(cls.META_SCHEMA).iter_errors(schema):
raise exceptions.SchemaError.create_from(error)

def evolve(self, **kwargs):
return attr.evolve(self, **kwargs)

def iter_errors(self, instance, _schema=None):
if _schema is None:
if _schema is not None:
warnings.warn(
(
"Passing a schema to Validator.iter_errors "
"is deprecated and will be removed in a future "
"release. Call validator.evolve(schema=new_schema)."
"iter_errors(...) instead."
),
DeprecationWarning,
)
else:
_schema = self.schema

if _schema is True:
Expand Down Expand Up @@ -214,7 +227,7 @@ def iter_errors(self, instance, _schema=None):
self.resolver.pop_scope()

def descend(self, instance, schema, path=None, schema_path=None):
for error in self.iter_errors(instance, schema):
for error in self.evolve(schema=schema).iter_errors(instance):
if path is not None:
error.path.appendleft(path)
if schema_path is not None:
Expand All @@ -232,7 +245,19 @@ def is_type(self, instance, type):
raise exceptions.UnknownType(type, instance, self.schema)

def is_valid(self, instance, _schema=None):
error = next(self.iter_errors(instance, _schema), None)
if _schema is not None:
warnings.warn(
(
"Passing a schema to Validator.is_valid is deprecated "
"and will be removed in a future release. Call "
"validator.evolve(schema=new_schema).is_valid(...) "
"instead."
),
DeprecationWarning,
)
self = self.evolve(schema=_schema)

error = next(self.iter_errors(instance), None)
return error is None

if version is not None:
Expand Down

0 comments on commit 996437f

Please sign in to comment.