From c0c9bff02d3eb8d9928fb838da01a88b6d3ee9e7 Mon Sep 17 00:00:00 2001 From: Nicolas Karolak Date: Fri, 14 Feb 2025 22:54:06 +0100 Subject: [PATCH] Allow to validate yaml file against its modeline schema --- src/check_jsonschema/cli/main_command.py | 11 +++++++++- src/check_jsonschema/cli/parse_result.py | 22 +++++++++++++------ .../schema_loader/__init__.py | 9 +++++++- src/check_jsonschema/schema_loader/main.py | 21 ++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/check_jsonschema/cli/main_command.py b/src/check_jsonschema/cli/main_command.py index 9e93ff1ff..883a22ddf 100644 --- a/src/check_jsonschema/cli/main_command.py +++ b/src/check_jsonschema/cli/main_command.py @@ -17,6 +17,7 @@ from ..schema_loader import ( BuiltinSchemaLoader, MetaSchemaLoader, + ModelineSchemaLoader, SchemaLoader, SchemaLoaderBase, ) @@ -112,6 +113,11 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str: type=click.Choice(BUILTIN_SCHEMA_CHOICES, case_sensitive=False), metavar="BUILTIN_SCHEMA_NAME", ) +@click.option( + "--modeline-schema", + is_flag=True, + help="Use the schema defined in the modeline.", +) @click.option( "--check-metaschema", is_flag=True, @@ -235,6 +241,7 @@ def main( schemafile: str | None, builtin_schema: str | None, base_uri: str | None, + modeline_schema: bool, check_metaschema: bool, no_cache: bool, cache_filename: str | None, @@ -255,7 +262,7 @@ def main( args.set_regex_variant(regex_variant, legacy_opt=format_regex) - args.set_schema(schemafile, builtin_schema, check_metaschema) + args.set_schema(schemafile, builtin_schema, check_metaschema, modeline_schema) args.set_validator(validator_class) args.base_uri = base_uri @@ -292,6 +299,8 @@ def main( def build_schema_loader(args: ParseResult) -> SchemaLoaderBase: if args.schema_mode == SchemaLoadingMode.metaschema: return MetaSchemaLoader(base_uri=args.base_uri) + if args.schema_mode == SchemaLoadingMode.modeline: + return ModelineSchemaLoader(instancefiles=args.instancefiles) elif args.schema_mode == SchemaLoadingMode.builtin: assert args.schema_path is not None return BuiltinSchemaLoader(args.schema_path, base_uri=args.base_uri) diff --git a/src/check_jsonschema/cli/parse_result.py b/src/check_jsonschema/cli/parse_result.py index bfd9065b1..78acde456 100644 --- a/src/check_jsonschema/cli/parse_result.py +++ b/src/check_jsonschema/cli/parse_result.py @@ -15,6 +15,7 @@ class SchemaLoadingMode(enum.Enum): filepath = "filepath" builtin = "builtin" metaschema = "metaschema" + modeline = "modeline" class ParseResult: @@ -56,20 +57,25 @@ def set_regex_variant( self.regex_variant = RegexVariantName(variant_name) def set_schema( - self, schemafile: str | None, builtin_schema: str | None, check_metaschema: bool + self, + schemafile: str | None, + builtin_schema: str | None, + check_metaschema: bool, + modeline_schema: bool, ) -> None: mutex_arg_count = sum( - 1 if x else 0 for x in (schemafile, builtin_schema, check_metaschema) + 1 if x else 0 + for x in (schemafile, builtin_schema, check_metaschema, modeline_schema) ) if mutex_arg_count == 0: raise click.UsageError( - "Either --schemafile, --builtin-schema, or --check-metaschema " - "must be provided" + "Either --schemafile, --builtin-schema, --check-metaschema, " + "or --modeline-schema must be provided" ) if mutex_arg_count > 1: raise click.UsageError( - "--schemafile, --builtin-schema, and --check-metaschema " - "are mutually exclusive" + "--schemafile, --builtin-schema, --check-metaschema, " + "and --modeline-schema are mutually exclusive" ) if schemafile: @@ -78,8 +84,10 @@ def set_schema( elif builtin_schema: self.schema_mode = SchemaLoadingMode.builtin self.schema_path = builtin_schema - else: + elif check_metaschema: self.schema_mode = SchemaLoadingMode.metaschema + else: + self.schema_mode = SchemaLoadingMode.modeline def set_validator( self, validator_class: type[jsonschema.protocols.Validator] | None diff --git a/src/check_jsonschema/schema_loader/__init__.py b/src/check_jsonschema/schema_loader/__init__.py index 0a55e06bc..0feb6e847 100644 --- a/src/check_jsonschema/schema_loader/__init__.py +++ b/src/check_jsonschema/schema_loader/__init__.py @@ -1,11 +1,18 @@ from .errors import SchemaParseError, UnsupportedUrlScheme -from .main import BuiltinSchemaLoader, MetaSchemaLoader, SchemaLoader, SchemaLoaderBase +from .main import ( + BuiltinSchemaLoader, + MetaSchemaLoader, + ModelineSchemaLoader, + SchemaLoader, + SchemaLoaderBase, +) __all__ = ( "SchemaParseError", "UnsupportedUrlScheme", "BuiltinSchemaLoader", "MetaSchemaLoader", + "ModelineSchemaLoader", "SchemaLoader", "SchemaLoaderBase", ) diff --git a/src/check_jsonschema/schema_loader/main.py b/src/check_jsonschema/schema_loader/main.py index e056389a9..39837008c 100644 --- a/src/check_jsonschema/schema_loader/main.py +++ b/src/check_jsonschema/schema_loader/main.py @@ -2,6 +2,7 @@ import functools import pathlib +import re import typing as t import urllib.error import urllib.parse @@ -233,6 +234,26 @@ def _dialect_of_schema(schema: dict[str, t.Any] | bool) -> str | None: return schema_dialect +class ModelineSchemaLoader(SchemaLoader): + def __init__(self, *, instancefiles: tuple[t.IO[bytes], ...] | None = None) -> None: + if not instancefiles: + instancefiles = () + if len(instancefiles) > 1: + raise NotImplementedError( + "'--modeline-schema' cannot be used on multiple files simultaneously." + ) + self.schemafile = self._get_schema_from_modeline(instancefiles[0]) + super().__init__(self.schemafile) + + def _get_schema_from_modeline(self, instancefile: t.IO[bytes]) -> str: + modeline = instancefile.readline() + pattern = r"^# yaml-language-server: \$schema=(?P.*)$" + match = re.match(pattern, modeline.decode()) + if not match: + raise Exception("Modeline with schema not found.") + return match.group("schema") + + class BuiltinSchemaLoader(SchemaLoader): def __init__(self, schema_name: str, *, base_uri: str | None = None) -> None: self.schema_name = schema_name