Skip to content

Commit eed1aea

Browse files
authored
Fix how '--check-metaschema' builds validators (#230)
* Simplify: remove `make_validator` This was an intermediate method which doesn't appear to be necessary, and leverages caching behavior which is never used. Remove it and move the body into `get_validator` (its only caller). * Minor cleanup to acceptance test naming * Fix how '--check-metaschema' builds a validator Metaschema checking was not building the validator correctly. Primarily two fixes are applied here: - the metaschema's schema dialect is no longer copied from the schema being checked, but is taken from the metaschema document - metaschema checking now automatically includes format checking and applies the CLI parameters for format checking (including the ability to disable format checking) Add test for an invalid format under metaschema. This test requires one of the format checking libraries, and therefore drives the need for new features in the testsuite, including the addition of config for the example file tests. Example file config is schema-validated, which serves as another dogfooding self-check. The test file imitates the driving use-case in #220 The acceptance test definition is refactored to make managing the test data a little easier.
1 parent f39794d commit eed1aea

File tree

9 files changed

+334
-32
lines changed

9 files changed

+334
-32
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ repos:
66
- id: check-dependabot
77
- id: check-github-workflows
88
- id: check-readthedocs
9-
- id: check-jsonschema
9+
- id: check-metaschema
1010
name: Validate Vendored Schemas
11-
args: ["--check-metaschema"]
1211
files: ^src/check_jsonschema/builtin_schemas/vendor/.*\.json$
12+
- id: check-jsonschema
13+
name: Validate Test Configs
14+
args: ["--schemafile", "tests/example-files/config_schema.json"]
15+
files: ^tests/example-files/.*/_config.yaml$
1316
- repo: https://github.com/pre-commit/pre-commit-hooks.git
1417
rev: v4.4.0
1518
hooks:

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Unreleased
1111
.. vendor-insert-here
1212
1313
- Update vendored schemas (2023-01-18)
14+
- Fix a bug in which `--check-metaschema` was not building validators correctly.
15+
The metaschema's schema dialect is chosen correctly now, and metaschema
16+
formats are now checked by default. This can be disabled with
17+
`--disable-format`.
1418

1519
0.20.0
1620
------

src/check_jsonschema/schema_loader/main.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ def __init__(
7474
# setup a schema reader lazily, when needed
7575
self._reader: LocalSchemaReader | HttpSchemaReader | None = None
7676

77-
# setup a location to store the validator so that it is only built once by default
78-
self._validator: jsonschema.Validator | None = None
79-
8077
@property
8178
def reader(self) -> LocalSchemaReader | HttpSchemaReader:
8279
if self._reader is None:
@@ -105,8 +102,12 @@ def get_schema_ref_base(self) -> str | None:
105102
def get_schema(self) -> dict[str, t.Any]:
106103
return self.reader.read_schema()
107104

108-
def make_validator(
109-
self, format_opts: FormatOptions, fill_defaults: bool
105+
def get_validator(
106+
self,
107+
path: pathlib.Path,
108+
instance_doc: dict[str, t.Any],
109+
format_opts: FormatOptions,
110+
fill_defaults: bool,
110111
) -> jsonschema.Validator:
111112
schema_uri = self.get_schema_ref_base()
112113
schema = self.get_schema()
@@ -138,16 +139,6 @@ def make_validator(
138139
)
139140
return t.cast(jsonschema.Validator, validator)
140141

141-
def get_validator(
142-
self,
143-
path: pathlib.Path,
144-
instance_doc: dict[str, t.Any],
145-
format_opts: FormatOptions,
146-
fill_defaults: bool,
147-
) -> jsonschema.Validator:
148-
self._validator = self.make_validator(format_opts, fill_defaults)
149-
return self._validator
150-
151142

152143
class BuiltinSchemaLoader(SchemaLoader):
153144
def __init__(self, schema_name: str) -> None:
@@ -168,5 +159,16 @@ def get_validator(
168159
format_opts: FormatOptions,
169160
fill_defaults: bool,
170161
) -> jsonschema.Validator:
171-
validator = jsonschema.validators.validator_for(instance_doc)
172-
return t.cast(jsonschema.Validator, validator(validator.META_SCHEMA))
162+
schema_validator = jsonschema.validators.validator_for(instance_doc)
163+
meta_validator_class = jsonschema.validators.validator_for(
164+
schema_validator.META_SCHEMA, default=schema_validator
165+
)
166+
167+
# format checker (which may be None)
168+
meta_schema_dialect = schema_validator.META_SCHEMA.get("$schema")
169+
format_checker = make_format_checker(format_opts, meta_schema_dialect)
170+
171+
meta_validator = meta_validator_class(
172+
schema_validator.META_SCHEMA, format_checker=format_checker
173+
)
174+
return meta_validator

tests/acceptance/test_example_files.py

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import importlib.util
15
import shlex
26
from pathlib import Path
37

@@ -36,7 +40,9 @@ def _build_hook_cases(category):
3640
example_dir = EXAMPLE_HOOK_FILES / category / hookid
3741
if example_dir.exists():
3842
for example in example_dir.iterdir():
39-
res[str(example.relative_to(EXAMPLE_HOOK_FILES))] = hookid
43+
if example.name == "_config.yaml":
44+
continue
45+
res[str(example.relative_to(EXAMPLE_HOOK_FILES / category))] = hookid
4046
return res
4147

4248

@@ -48,37 +54,31 @@ def _get_explicit_cases(category):
4854
return res
4955

5056

51-
def _check_case_skip(case_name):
52-
if case_name.endswith("json5") and not JSON5_ENABLED:
53-
pytest.skip("cannot check json5 support without json5 enabled")
54-
if case_name.endswith("toml") and not TOML_ENABLED:
55-
pytest.skip("cannot check toml support without toml enabled")
56-
57-
5857
POSITIVE_HOOK_CASES = _build_hook_cases("positive")
5958
NEGATIVE_HOOK_CASES = _build_hook_cases("negative")
6059

6160

6261
@pytest.mark.parametrize("case_name", POSITIVE_HOOK_CASES.keys())
6362
def test_hook_positive_examples(case_name, run_line):
64-
_check_case_skip(case_name)
63+
rcase = ResolvedCase.load_positive(case_name)
6564

6665
hook_id = POSITIVE_HOOK_CASES[case_name]
67-
ret = run_line(HOOK_CONFIG[hook_id] + [str(EXAMPLE_HOOK_FILES / case_name)])
66+
ret = run_line(HOOK_CONFIG[hook_id] + [rcase.path] + rcase.add_args)
6867
assert ret.exit_code == 0
6968

7069

7170
@pytest.mark.parametrize("case_name", NEGATIVE_HOOK_CASES.keys())
7271
def test_hook_negative_examples(case_name, run_line):
73-
_check_case_skip(case_name)
72+
rcase = ResolvedCase.load_negative(case_name)
73+
7474
hook_id = NEGATIVE_HOOK_CASES[case_name]
75-
ret = run_line(HOOK_CONFIG[hook_id] + [str(EXAMPLE_HOOK_FILES / case_name)])
75+
ret = run_line(HOOK_CONFIG[hook_id] + [rcase.path] + rcase.add_args)
7676
assert ret.exit_code == 1
7777

7878

7979
@pytest.mark.parametrize("case_name", _get_explicit_cases("positive"))
8080
def test_explicit_positive_examples(case_name, run_line):
81-
_check_case_skip(case_name)
81+
_check_file_format_skip(case_name)
8282
casedir = EXAMPLE_EXPLICIT_FILES / "positive" / case_name
8383

8484
instance = casedir / "instance.json"
@@ -104,3 +104,66 @@ def test_explicit_positive_examples(case_name, run_line):
104104
]
105105
)
106106
assert ret.exit_code == 0
107+
108+
109+
def _check_file_format_skip(case_name):
110+
if case_name.endswith("json5") and not JSON5_ENABLED:
111+
pytest.skip("cannot check json5 support without json5 enabled")
112+
if case_name.endswith("toml") and not TOML_ENABLED:
113+
pytest.skip("cannot check toml support without toml enabled")
114+
115+
116+
@dataclasses.dataclass
117+
class ResolvedCase:
118+
category: str
119+
path: str
120+
add_args: list[str]
121+
config: dict
122+
123+
def check_skip(self) -> None:
124+
if "requires_packages" in self.config:
125+
for pkg in self.config["requires_packages"]:
126+
if _package_is_installed(pkg):
127+
continue
128+
pytest.skip(f"cannot check because '{pkg}' is not installed")
129+
130+
def __post_init__(self) -> None:
131+
self.check_skip()
132+
133+
@classmethod
134+
def load_positive(cls: type[ResolvedCase], case_name: str) -> ResolvedCase:
135+
return cls._load("positive", case_name)
136+
137+
@classmethod
138+
def load_negative(cls: type[ResolvedCase], case_name: str) -> ResolvedCase:
139+
return cls._load("negative", case_name)
140+
141+
@classmethod
142+
def _load(cls: type[ResolvedCase], category: str, case_name: str) -> ResolvedCase:
143+
_check_file_format_skip(case_name)
144+
145+
path = EXAMPLE_HOOK_FILES / category / case_name
146+
config = cls._load_file_config(path.parent / "_config.yaml", path.name)
147+
148+
return cls(
149+
category=category,
150+
path=str(path),
151+
add_args=config.get("add_args", []),
152+
config=config,
153+
)
154+
155+
@staticmethod
156+
def _load_file_config(config_path, name):
157+
if not config_path.is_file():
158+
return {}
159+
with open(config_path) as fp:
160+
loaded_conf = yaml.load(fp)
161+
files_section = loaded_conf.get("files", {})
162+
return files_section.get(name, {})
163+
164+
165+
def _package_is_installed(pkg: str) -> bool:
166+
spec = importlib.util.find_spec(pkg)
167+
if spec is None:
168+
return False
169+
return True
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$comment": "An internal schema used to check the testsuite _config.yaml files.",
3+
"$schema": "https://json-schema.org/draft/2020-12/schema",
4+
"$defs": {
5+
"spec": {
6+
"type": "object",
7+
"properties": {
8+
"requires_packages": {
9+
"type": "array",
10+
"items": {
11+
"type": "string"
12+
}
13+
},
14+
"add_args": {
15+
"type": "array",
16+
"items": {
17+
"type": "string"
18+
}
19+
}
20+
},
21+
"additionalProperties": false
22+
}
23+
},
24+
"type": "object",
25+
"properties": {
26+
"files": {
27+
"type": "object",
28+
"patternProperties": {
29+
"^.+\\.(json|yml|yaml|json5|toml)$": {
30+
"$ref": "#/$defs/spec"
31+
}
32+
},
33+
"additionalProperties": false
34+
}
35+
},
36+
"additionalProperties": false
37+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "Meta-schema defining a ref with invalid URI reference",
4+
"$defs": {
5+
"prop<(str|list)>": {
6+
"oneOf": [
7+
{
8+
"type": "string"
9+
},
10+
{
11+
"type": "array",
12+
"items": true
13+
}
14+
]
15+
},
16+
"anchorString": {
17+
"type": "string",
18+
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
19+
},
20+
"uriString": {
21+
"type": "string",
22+
"format": "uri"
23+
},
24+
"uriReferenceString": {
25+
"type": "string",
26+
"format": "uri-reference"
27+
},
28+
"original2020metaschema": {
29+
"$schema": "https://json-schema.org/draft/2020-12/schema",
30+
"$vocabulary": {
31+
"https://json-schema.org/draft/2020-12/vocab/core": true
32+
},
33+
"$dynamicAnchor": "meta",
34+
"title": "Core vocabulary meta-schema",
35+
"type": [
36+
"object",
37+
"boolean"
38+
],
39+
"properties": {
40+
"$id": {
41+
"$ref": "#/$defs/uriReferenceString",
42+
"$comment": "Non-empty fragments not allowed.",
43+
"pattern": "^[^#]*#?$"
44+
},
45+
"$schema": {
46+
"$ref": "#/$defs/uriString"
47+
},
48+
"$ref": {
49+
"$ref": "#/$defs/uriReferenceString"
50+
},
51+
"$anchor": {
52+
"$ref": "#/$defs/anchorString"
53+
},
54+
"$dynamicRef": {
55+
"$ref": "#/$defs/uriReferenceString"
56+
},
57+
"$dynamicAnchor": {
58+
"$ref": "#/$defs/anchorString"
59+
},
60+
"$vocabulary": {
61+
"type": "object",
62+
"propertyNames": {
63+
"$ref": "#/$defs/uriString"
64+
},
65+
"additionalProperties": {
66+
"type": "boolean"
67+
}
68+
},
69+
"$comment": {
70+
"type": "string"
71+
},
72+
"$defs": {
73+
"type": "object",
74+
"additionalProperties": {
75+
"$dynamicRef": "#meta"
76+
}
77+
}
78+
}
79+
}
80+
},
81+
"allOf": [
82+
{
83+
"$ref": "#/$defs/original2020metaschema"
84+
},
85+
{
86+
"properties": {
87+
"title": {
88+
"$ref": "#/$defs/prop<(str|list)>"
89+
}
90+
}
91+
}
92+
]
93+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
files:
2+
2020_invalid_format_value.json:
3+
requires_packages:
4+
- rfc3987

0 commit comments

Comments
 (0)