Skip to content

Commit 819d834

Browse files
committed
Instrument regress-backed pattern evaluation
When an alternate RegexImplementation is selected, it will be passed down to the code which builds a Validator. The Validator is extended with the keyword validator provided by the RegexImplementation. Because this uses the `extend()` interface, a test which subclassed a validator broke -- this is documented in `jsonschema` as unsupported usage, so the test simply had to be updated to use supported interfaces.
1 parent 802c5e4 commit 819d834

File tree

7 files changed

+86
-26
lines changed

7 files changed

+86
-26
lines changed

src/check_jsonschema/checker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .formats import FormatOptions
1212
from .instance_loader import InstanceLoader
1313
from .parsers import ParseError
14+
from .regex_variants import RegexImplementation
1415
from .reporter import Reporter
1516
from .result import CheckResult
1617
from .schema_loader import SchemaLoaderBase, SchemaParseError, UnsupportedUrlScheme
@@ -29,14 +30,16 @@ def __init__(
2930
reporter: Reporter,
3031
*,
3132
format_opts: FormatOptions,
33+
regex_impl: RegexImplementation,
3234
traceback_mode: str = "short",
3335
fill_defaults: bool = False,
3436
) -> None:
3537
self._schema_loader = schema_loader
3638
self._instance_loader = instance_loader
3739
self._reporter = reporter
3840

39-
self._format_opts = format_opts if format_opts is not None else FormatOptions()
41+
self._format_opts = format_opts
42+
self._regex_impl = regex_impl
4043
self._traceback_mode = traceback_mode
4144
self._fill_defaults = fill_defaults
4245

@@ -51,7 +54,7 @@ def get_validator(
5154
) -> jsonschema.protocols.Validator:
5255
try:
5356
return self._schema_loader.get_validator(
54-
path, doc, self._format_opts, self._fill_defaults
57+
path, doc, self._format_opts, self._regex_impl, self._fill_defaults
5558
)
5659
except SchemaParseError as e:
5760
self._fail("Error: schemafile could not be parsed as JSON", e)

src/check_jsonschema/cli/main_command.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99

1010
from ..catalog import CUSTOM_SCHEMA_NAMES, SCHEMA_CATALOG
1111
from ..checker import SchemaChecker
12-
from ..formats import KNOWN_FORMATS, RegexVariantName
12+
from ..formats import KNOWN_FORMATS
1313
from ..instance_loader import InstanceLoader
1414
from ..parsers import SUPPORTED_FILE_FORMATS
15+
from ..regex_variants import RegexImplementation, RegexVariantName
1516
from ..reporter import REPORTER_BY_NAME, Reporter
1617
from ..schema_loader import (
1718
BuiltinSchemaLoader,
@@ -327,6 +328,7 @@ def build_checker(args: ParseResult) -> SchemaChecker:
327328
instance_loader,
328329
reporter,
329330
format_opts=args.format_opts,
331+
regex_impl=RegexImplementation(args.regex_variant),
330332
traceback_mode=args.traceback_mode,
331333
fill_defaults=args.fill_defaults,
332334
)

src/check_jsonschema/cli/parse_result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import jsonschema
99

1010
from ..formats import FormatOptions
11-
from ..regex_variants import RegexVariantName
11+
from ..regex_variants import RegexImplementation, RegexVariantName
1212
from ..transforms import Transform
1313

1414
if sys.version_info >= (3, 8):
@@ -99,7 +99,7 @@ def set_validator(
9999
@property
100100
def format_opts(self) -> FormatOptions:
101101
return FormatOptions(
102+
regex_impl=RegexImplementation(self.regex_variant),
102103
enabled=not self.disable_all_formats,
103-
regex_variant=self.regex_variant,
104104
disabled_formats=self.disable_formats,
105105
)

src/check_jsonschema/formats/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import jsonschema
66
import jsonschema.validators
77

8-
from ..regex_variants import RegexImplementation, RegexVariantName
8+
from ..regex_variants import RegexImplementation
99
from .implementations import validate_rfc3339, validate_time
1010

1111
# all known format strings except for a selection from draft3 which have either
@@ -40,12 +40,12 @@ class FormatOptions:
4040
def __init__(
4141
self,
4242
*,
43+
regex_impl: RegexImplementation,
4344
enabled: bool = True,
44-
regex_variant: RegexVariantName = RegexVariantName.default,
4545
disabled_formats: tuple[str, ...] = (),
4646
) -> None:
4747
self.enabled = enabled
48-
self.regex_variant = regex_variant
48+
self.regex_impl = regex_impl
4949
self.disabled_formats = disabled_formats
5050

5151

@@ -72,8 +72,7 @@ def make_format_checker(
7272

7373
# replace the regex check
7474
del checker.checkers["regex"]
75-
regex_impl = RegexImplementation(opts.regex_variant)
76-
checker.checks("regex")(regex_impl.check_format)
75+
checker.checks("regex")(opts.regex_impl.check_format)
7776
checker.checks("date-time")(validate_rfc3339)
7877
checker.checks("time")(validate_time)
7978

src/check_jsonschema/regex_variants.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import typing as t
44

5+
import jsonschema
56
import regress
67

78

@@ -29,3 +30,32 @@ def check_format(self, instance: t.Any) -> bool:
2930
return False
3031

3132
return True
33+
34+
def pattern_keyword(
35+
self, validator: t.Any, pattern: str, instance: str, schema: t.Any
36+
) -> t.Iterator[jsonschema.ValidationError]:
37+
if not validator.is_type(instance, "string"):
38+
return
39+
40+
if self.variant == RegexVariantName.default:
41+
try:
42+
regress_pattern = regress.Regex(pattern)
43+
except regress.RegressError: # type: ignore[attr-defined]
44+
yield jsonschema.ValidationError(
45+
f"pattern {pattern!r} failed to compile"
46+
)
47+
if not regress_pattern.find(instance):
48+
yield jsonschema.ValidationError(
49+
f"{instance!r} does not match {pattern!r}"
50+
)
51+
else:
52+
try:
53+
re_pattern = re.compile(pattern)
54+
except re.error:
55+
yield jsonschema.ValidationError(
56+
f"pattern {pattern!r} failed to compile"
57+
)
58+
if not re_pattern.search(instance):
59+
yield jsonschema.ValidationError(
60+
f"{instance!r} does not match {pattern!r}"
61+
)

src/check_jsonschema/schema_loader/main.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ..builtin_schemas import get_builtin_schema
1212
from ..formats import FormatOptions, make_format_checker
1313
from ..parsers import ParserSet
14+
from ..regex_variants import RegexImplementation
1415
from ..utils import is_url_ish
1516
from .errors import UnsupportedUrlScheme
1617
from .readers import HttpSchemaReader, LocalSchemaReader, StdinSchemaReader
@@ -45,12 +46,23 @@ def set_defaults_then_validate(
4546
)
4647

4748

49+
def _extend_with_pattern_implementation(
50+
validator_class: type[jsonschema.protocols.Validator],
51+
regex_impl: RegexImplementation,
52+
) -> type[jsonschema.Validator]:
53+
return jsonschema.validators.extend(
54+
validator_class,
55+
{"pattern": regex_impl.pattern_keyword},
56+
)
57+
58+
4859
class SchemaLoaderBase:
4960
def get_validator(
5061
self,
5162
path: pathlib.Path | str,
5263
instance_doc: dict[str, t.Any],
5364
format_opts: FormatOptions,
65+
regex_impl: RegexImplementation,
5466
fill_defaults: bool,
5567
) -> jsonschema.protocols.Validator:
5668
raise NotImplementedError
@@ -124,14 +136,16 @@ def get_validator(
124136
path: pathlib.Path | str,
125137
instance_doc: dict[str, t.Any],
126138
format_opts: FormatOptions,
139+
regex_impl: RegexImplementation,
127140
fill_defaults: bool,
128141
) -> jsonschema.protocols.Validator:
129-
return self._get_validator(format_opts, fill_defaults)
142+
return self._get_validator(format_opts, regex_impl, fill_defaults)
130143

131144
@functools.lru_cache
132145
def _get_validator(
133146
self,
134147
format_opts: FormatOptions,
148+
regex_impl: RegexImplementation,
135149
fill_defaults: bool,
136150
) -> jsonschema.protocols.Validator:
137151
retrieval_uri = self.get_schema_retrieval_uri()
@@ -168,6 +182,9 @@ def _get_validator(
168182
if fill_defaults:
169183
validator_cls = _extend_with_default(validator_cls)
170184

185+
# set the regex variant for 'pattern' keywords
186+
validator_cls = _extend_with_pattern_implementation(validator_cls, regex_impl)
187+
171188
# now that we know it's safe to try to create the validator instance, do it
172189
validator = validator_cls(
173190
schema,
@@ -206,6 +223,7 @@ def get_validator(
206223
path: pathlib.Path | str,
207224
instance_doc: dict[str, t.Any],
208225
format_opts: FormatOptions,
226+
regex_impl: RegexImplementation,
209227
fill_defaults: bool,
210228
) -> jsonschema.protocols.Validator:
211229
schema_validator = jsonschema.validators.validator_for(instance_doc)

tests/acceptance/test_custom_validator_class.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,32 @@ def _foo_module(mock_module):
6666
"""\
6767
import jsonschema
6868
69-
class MyValidator:
70-
def __init__(self, schema, *args, **kwargs):
71-
self.schema = schema
72-
self.real_validator = jsonschema.validators.Draft7Validator(
73-
schema, *args, **kwargs
74-
)
75-
76-
def iter_errors(self, data, *args, **kwargs):
77-
yield from self.real_validator.iter_errors(data, *args, **kwargs)
78-
for event in data["events"]:
79-
if "Occult" in event["title"]:
69+
70+
def check_occult_properties(validator, properties, instance, schema):
71+
if not validator.is_type(instance, "object"):
72+
return
73+
74+
for property, subschema in properties.items():
75+
if property in instance:
76+
if property == "title" and "Occult" in instance["title"]:
8077
yield jsonschema.exceptions.ValidationError(
8178
"Error! Occult event detected! Run!",
82-
validator=None,
79+
validator=validator,
8380
validator_value=None,
84-
instance=event,
85-
schema=self.schema,
81+
instance=instance,
82+
schema=schema,
8683
)
84+
yield from validator.descend(
85+
instance[property],
86+
subschema,
87+
path=property,
88+
schema_path=property,
89+
)
90+
91+
MyValidator = jsonschema.validators.extend(
92+
jsonschema.validators.Draft7Validator,
93+
{"properties": check_occult_properties},
94+
)
8795
""",
8896
)
8997

@@ -115,7 +123,7 @@ def test_custom_validator_class_can_detect_custom_conditions(run_line, tmp_path)
115123
str(doc),
116124
],
117125
)
118-
assert result.exit_code == 1 # fail
126+
assert result.exit_code == 1, result.stdout # fail
119127
assert "Occult event detected" in result.stdout, result.stdout
120128

121129

0 commit comments

Comments
 (0)