Skip to content

Commit 94c60e4

Browse files
committed
Speed up Validator.evolve by pre-computing fields.
We're not a general class, so we know what fields we need ahead of time. This seems to give ~15% speedup on Validator evolution, which happens often as part of walking up and down schemas.
1 parent 2b547c7 commit 94c60e4

File tree

1 file changed

+61
-17
lines changed

1 file changed

+61
-17
lines changed

jsonschema/validators.py

+61-17
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,23 @@ def __init_subclass__(cls):
217217
stacklevel=2,
218218
)
219219

220+
def evolve(self, **changes):
221+
cls = self.__class__
222+
schema = changes.setdefault("schema", self.schema)
223+
NewValidator = validator_for(schema, default=cls)
224+
225+
for field in attr.fields(cls):
226+
if not field.init:
227+
continue
228+
attr_name = field.name
229+
init_name = field.alias
230+
if init_name not in changes:
231+
changes[init_name] = getattr(self, attr_name)
232+
233+
return NewValidator(**changes)
234+
235+
cls.evolve = evolve
236+
220237
def __attrs_post_init__(self):
221238
if self._resolver is None:
222239
self._resolver = self._registry.resolver_with_root(
@@ -257,18 +274,10 @@ def resolver(self):
257274
return self._ref_resolver
258275

259276
def evolve(self, **changes):
260-
# Essentially reproduces attr.evolve, but may involve instantiating
261-
# a different class than this one.
262-
cls = self.__class__
263-
264277
schema = changes.setdefault("schema", self.schema)
265-
NewValidator = validator_for(schema, default=cls)
278+
NewValidator = validator_for(schema, default=self.__class__)
266279

267-
for field in attr.fields(cls):
268-
if not field.init:
269-
continue
270-
attr_name = field.name # To deal with private attributes.
271-
init_name = field.alias
280+
for (attr_name, init_name) in evolve_fields:
272281
if init_name not in changes:
273282
changes[init_name] = getattr(self, attr_name)
274283

@@ -328,17 +337,46 @@ def descend(
328337
schema_path=None,
329338
resolver=None,
330339
):
340+
if schema is True:
341+
return
342+
elif schema is False:
343+
yield exceptions.ValidationError(
344+
f"False schema does not allow {instance!r}",
345+
validator=None,
346+
validator_value=None,
347+
instance=instance,
348+
schema=schema,
349+
)
350+
return
351+
331352
if resolver is None:
332353
resolver = self._resolver.in_subresource(
333354
specification.create_resource(schema),
334355
)
335-
validator = self.evolve(schema=schema, _resolver=resolver)
336-
for error in validator.iter_errors(instance):
337-
if path is not None:
338-
error.path.appendleft(path)
339-
if schema_path is not None:
340-
error.schema_path.appendleft(schema_path)
341-
yield error
356+
evolved = self.evolve(schema=schema, _resolver=resolver)
357+
358+
for k, v in applicable_validators(schema):
359+
validator = evolved.VALIDATORS.get(k)
360+
if validator is None:
361+
continue
362+
363+
errors = validator(evolved, v, instance, schema) or ()
364+
for error in errors:
365+
# set details if not already set by the called fn
366+
error._set(
367+
validator=k,
368+
validator_value=v,
369+
instance=instance,
370+
schema=schema,
371+
type_checker=evolved.TYPE_CHECKER,
372+
)
373+
if k not in {"if", "$ref"}:
374+
error.schema_path.appendleft(k)
375+
if path is not None:
376+
error.path.appendleft(path)
377+
if schema_path is not None:
378+
error.schema_path.appendleft(schema_path)
379+
yield error
342380

343381
def validate(self, *args, **kwargs):
344382
for error in self.iter_errors(*args, **kwargs):
@@ -389,6 +427,12 @@ def is_valid(self, instance, _schema=None):
389427
error = next(self.iter_errors(instance), None)
390428
return error is None
391429

430+
evolve_fields = [
431+
(field.name, field.alias)
432+
for field in attr.fields(Validator)
433+
if field.init
434+
]
435+
392436
if version is not None:
393437
safe = version.title().replace(" ", "").replace("-", "")
394438
Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"

0 commit comments

Comments
 (0)