From da3a6af6b98808fd8fd2fd43513cc04f0e53df5e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Aug 2023 11:28:14 -0500 Subject: [PATCH] Add support for remote YAML/TOML/JSON5 schemas Also fix a new deprecation warning about the import path for `jsonschema.protocols.Validator`. --- CHANGELOG.rst | 3 +++ src/check_jsonschema/checker.py | 2 +- src/check_jsonschema/schema_loader/main.py | 10 +++++----- src/check_jsonschema/schema_loader/readers.py | 15 ++++++++++++--- tests/unit/test_schema_loader.py | 19 +++++++++++++------ 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8df1f583..02140831e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,9 @@ Unreleased non-JSON format supported by `check-jsonschema`. The file type is inferred only from the file extension in these cases and defaults to JSON if there is no recognizable extension. +- Remote schemafiles (http/s) now support YAML, TOML, and JSON5 formats, if the + URL ends with the appropriate extension and the matching parser is available. + Extensionless URLs are treated as JSON. 0.23.3 ------ diff --git a/src/check_jsonschema/checker.py b/src/check_jsonschema/checker.py index bf1aea0a8..28931d5ff 100644 --- a/src/check_jsonschema/checker.py +++ b/src/check_jsonschema/checker.py @@ -48,7 +48,7 @@ def _fail(self, msg: str, err: Exception | None = None) -> t.NoReturn: def get_validator( self, path: pathlib.Path, doc: dict[str, t.Any] - ) -> jsonschema.Validator: + ) -> jsonschema.protocols.Validator: try: return self._schema_loader.get_validator( path, doc, self._format_opts, self._fill_defaults diff --git a/src/check_jsonschema/schema_loader/main.py b/src/check_jsonschema/schema_loader/main.py index c8c9045a1..4fa1c574e 100644 --- a/src/check_jsonschema/schema_loader/main.py +++ b/src/check_jsonschema/schema_loader/main.py @@ -17,7 +17,7 @@ def _extend_with_default( - validator_class: type[jsonschema.Validator], + validator_class: type[jsonschema.protocols.Validator], ) -> type[jsonschema.Validator]: validate_properties = validator_class.VALIDATORS["properties"] @@ -51,7 +51,7 @@ def get_validator( instance_doc: dict[str, t.Any], format_opts: FormatOptions, fill_defaults: bool, - ) -> jsonschema.Validator: + ) -> jsonschema.protocols.Validator: raise NotImplementedError @@ -112,7 +112,7 @@ def get_validator( instance_doc: dict[str, t.Any], format_opts: FormatOptions, fill_defaults: bool, - ) -> jsonschema.Validator: + ) -> jsonschema.protocols.Validator: retrieval_uri = self.get_schema_retrieval_uri() schema = self.get_schema() @@ -141,7 +141,7 @@ def get_validator( registry=reference_registry, format_checker=format_checker, ) - return t.cast(jsonschema.Validator, validator) + return t.cast(jsonschema.protocols.Validator, validator) class BuiltinSchemaLoader(SchemaLoader): @@ -163,7 +163,7 @@ def get_validator( instance_doc: dict[str, t.Any], format_opts: FormatOptions, fill_defaults: bool, - ) -> jsonschema.Validator: + ) -> jsonschema.protocols.Validator: schema_validator = jsonschema.validators.validator_for(instance_doc) meta_validator_class = jsonschema.validators.validator_for( schema_validator.META_SCHEMA, default=schema_validator diff --git a/src/check_jsonschema/schema_loader/readers.py b/src/check_jsonschema/schema_loader/readers.py index c2e469781..4c362dc8d 100644 --- a/src/check_jsonschema/schema_loader/readers.py +++ b/src/check_jsonschema/schema_loader/readers.py @@ -1,6 +1,6 @@ from __future__ import annotations -import json +import io import typing as t import ruamel.yaml @@ -48,19 +48,28 @@ def __init__( disable_cache: bool, ) -> None: self.url = url + self.parsers = ParserSet() self.downloader = CacheDownloader( url, cache_filename, disable_cache=disable_cache, - validation_callback=json.loads, + validation_callback=self._parse, ) + self._parsed_schema: t.Any | None = None + + def _parse(self, schema_bytes: bytes) -> t.Any: + if self._parsed_schema is None: + self._parsed_schema = self.parsers.parse_data_with_path( + io.BytesIO(schema_bytes), self.url, default_filetype="json" + ) + return self._parsed_schema def get_retrieval_uri(self) -> str: return self.url def _read_impl(self) -> t.Any: with self.downloader.open() as fp: - return json.load(fp) + return self._parse(fp.read()) def read_schema(self) -> dict: return _run_load_callback(self.url, self._read_impl) diff --git a/tests/unit/test_schema_loader.py b/tests/unit/test_schema_loader.py index b6938cbd9..ddc487fa6 100644 --- a/tests/unit/test_schema_loader.py +++ b/tests/unit/test_schema_loader.py @@ -2,6 +2,7 @@ import pathlib import pytest +import responses from check_jsonschema.schema_loader import SchemaLoader, SchemaParseError from check_jsonschema.schema_loader.readers import HttpSchemaReader, LocalSchemaReader @@ -40,12 +41,12 @@ def test_schemaloader_path_handling_relative_local_path(in_tmp_dir, filename): [ "schema.yaml", "schema.yml", + "https://foo.example.com/schema.yaml", + "https://foo.example.com/schema.yml", ], ) -def test_schemaloader_local_yaml_data(tmp_path, filename): - f = tmp_path / filename - f.write_text( - """ +def test_schemaloader_yaml_data(tmp_path, filename): + schema_text = """ --- "$schema": https://json-schema.org/draft/2020-12/schema type: object @@ -60,8 +61,14 @@ def test_schemaloader_local_yaml_data(tmp_path, filename): c: type: string """ - ) - sl = SchemaLoader(str(f)) + if filename.startswith("http"): + responses.add("GET", filename, body=schema_text) + path = filename + else: + f = tmp_path / filename + f.write_text(schema_text) + path = str(f) + sl = SchemaLoader(path) schema = sl.get_schema() assert schema == { "$schema": "https://json-schema.org/draft/2020-12/schema",