Skip to content

Commit 5f73c0e

Browse files
committed
primitive Keyword object and handling solves the immediate need
1 parent ac10583 commit 5f73c0e

File tree

2 files changed

+110
-14
lines changed

2 files changed

+110
-14
lines changed

jsonschema/_keywords.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
13
from fractions import Fraction
24
import re
5+
import typing
36

47
from jsonschema._utils import (
58
ensure_list,
@@ -13,6 +16,58 @@
1316
from jsonschema.exceptions import FormatError, ValidationError
1417

1518

19+
class Keyword:
20+
"""
21+
The Keyword wraps a function to maintain full back-compatibility.
22+
23+
The class adds the ability to annotate the keyword implementation while
24+
behaving like a function, i.e. `Keyword(func)` behaves like `func` itself
25+
in most cases, and allows a more formal description of the behavior and
26+
dependencies.
27+
"""
28+
29+
def __init__(
30+
self,
31+
func: typing.Optional[callable] = None, /,
32+
needs: typing.Optional[typing.Iterable[str]] = (),
33+
):
34+
self.func = func
35+
self.needs = needs
36+
37+
def __call__(self, validator, property, instance, schema):
38+
if self.func is None:
39+
raise NotImplementedError(f"{self} has no validation method.")
40+
return self.func(validator, property, instance, schema)
41+
42+
43+
def keyword(func: typing.Optional[callable] = None, /, **kwargs):
44+
"""
45+
Syntactic sugar to decorate a function as a keyword.
46+
47+
The function is not modified, but wrapped by a Keyword object. This allows
48+
the same function to be reused as by multiple validators with different
49+
annotations.
50+
51+
Examples
52+
--------
53+
Functionally equivalent to the un-decorated function.
54+
>>> @keyword
55+
... def myKeyword(validator, property, instance, schema):
56+
... ...
57+
58+
Mark the keyword "else" to be evaluated after "if"
59+
>>> @keyword(needs=["if"])
60+
... def else_(validator, property, instance, schema):
61+
... ...
62+
63+
>>> kw = keyword(else_, needs="if")
64+
65+
"""
66+
if func is None:
67+
return lambda fun: Keyword(fun, **kwargs)
68+
return Keyword(func, **kwargs)
69+
70+
1671
def patternProperties(validator, patternProperties, instance, schema):
1772
if not validator.is_type(instance, "object"):
1873
return
@@ -389,6 +444,21 @@ def if_(validator, if_schema, instance, schema):
389444
yield from validator.descend(instance, else_, schema_path="else")
390445

391446

447+
@keyword(
448+
needs=[
449+
"prefixItems",
450+
"items",
451+
"contains",
452+
"allOf",
453+
"anyOf",
454+
"oneOf",
455+
"not",
456+
"if",
457+
"then",
458+
"else",
459+
"dependentSchemas",
460+
],
461+
)
392462
def unevaluatedItems(validator, unevaluatedItems, instance, schema):
393463
if not validator.is_type(instance, "array"):
394464
return
@@ -404,6 +474,21 @@ def unevaluatedItems(validator, unevaluatedItems, instance, schema):
404474
yield ValidationError(error % extras_msg(unevaluated_items))
405475

406476

477+
@keyword(
478+
needs=[
479+
"properties",
480+
"patternProperties",
481+
"additionalProperties",
482+
"allOf",
483+
"anyOf",
484+
"oneOf",
485+
"not",
486+
"if",
487+
"then",
488+
"else",
489+
"dependentSchemas",
490+
],
491+
)
407492
def unevaluatedProperties(validator, unevaluatedProperties, instance, schema):
408493
if not validator.is_type(instance, "object"):
409494
return

jsonschema/validators.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -379,20 +379,31 @@ def iter_errors(self, instance, _schema=None):
379379
)
380380
return
381381

382-
for validator, k, v in validators:
383-
errors = validator(self, v, instance, _schema) or ()
384-
for error in errors:
385-
# set details if not already set by the called fn
386-
error._set(
387-
validator=k,
388-
validator_value=v,
389-
instance=instance,
390-
schema=_schema,
391-
type_checker=self.TYPE_CHECKER,
392-
)
393-
if k not in {"if", "$ref"}:
394-
error.schema_path.appendleft(k)
395-
yield error
382+
todo = {k for _, k, _ in validators}
383+
while validators:
384+
dependant = []
385+
for validator, k, v in validators:
386+
if isinstance(validator, _keywords.Keyword) \
387+
and todo.intersection(validator.needs):
388+
dependant.append([validator, k, v])
389+
continue
390+
errors = validator(self, v, instance, _schema) or ()
391+
for error in errors:
392+
# set details if not already set by the called fn
393+
error._set(
394+
validator=k,
395+
validator_value=v,
396+
instance=instance,
397+
schema=_schema,
398+
type_checker=self.TYPE_CHECKER,
399+
)
400+
if k not in {"if", "$ref"}:
401+
error.schema_path.appendleft(k)
402+
yield error
403+
todo.discard(k)
404+
if dependant == validators:
405+
raise ValueError("Circular dependency between keywords")
406+
validators = dependant
396407

397408
def descend(
398409
self,

0 commit comments

Comments
 (0)